Безопасная аутентификация в Next.js (SSR & Middleware)
System Design / Backend

Безопасная аутентификация в Next.js (SSR & Middleware)

Н
Никита
23 дек. 2025 г.

1. Введение: Почему localStorage мертв

Раньше мы получали токен на клиенте и кидали его в локальное хранилище. В эру SPA (Single Page Applications) это работало. Но в Next.js мы рендерим страницы на сервере. Сервер не имеет доступа к localStorage.

Если ты попытаешься достать юзера в Server Component из локального хранилища, ты получишь null, страница отрендерится как для гостя, а потом на клиенте "моргнет" (flicker) и покажет контент юзера. Это UX уровень "джун".

Единственный путь джедая: Хранить сессию в Cookies. Они летают между клиентом и сервером автоматически. Supabase предоставляет пакет @supabase/ssr, который делает всю грязную работу за нас.



2. Технический разбор: PKCE Flow

Мы будем использовать PKCE (Proof Key for Code Exchange). Звучит страшно, но суть проста:

  1. Клиент просит ссылку на логин.

  1. Supabase возвращает одноразовый code.
  1. Сервер Next.js меняет этот code на session (access + refresh token) и записывает их в куки.

Шаг 1: Middleware (Страж ворот)

Это самый критичный кусок кода. Middleware в Next.js запускается перед каждым запросом. Его задача — обновить сессию (refresh token), если она протухла, и передать обновленные куки дальше.

src/middleware.ts:

TYPESCRIPT
import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase/middleware'

export async function middleware(request: NextRequest) {
  // Выносим логику в отдельный файл, чтобы middleware.ts оставался чистым
  return await updateSession(request)
}

export const config = {
  // Исключаем статику, картинки и фавиконки, чтобы не жечь ресурсы
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

src/lib/supabase/middleware.ts:

TYPESCRIPT
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function updateSession(request: NextRequest) {
  let response = NextResponse.next({
    request: { headers: request.headers },
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          // Здесь магия: обновляем куки и в реквесте, и в респонсе
          cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
          response = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            response.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // ВАЖНО: getUser валидирует токен на сервере Supabase. 
  // getSession просто парсит куку (это небезопасно для защиты роутов).
  const { data: { user } } = await supabase.auth.getUser()

  // Простой пример защиты админки
  if (request.nextUrl.pathname.startsWith('/dashboard') && !user) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return response
}

Шаг 2: Server Actions (Вход в систему)

Забудь про API Routes. Используем Server Actions — это нативные функции, которые вызываются из формы, но выполняются на сервере.

src/app/login/actions.ts:

TYPESCRIPT
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server' // Тот самый клиент из прошлой статьи

export async function login(formData: FormData) {
  const supabase = createClient()

  const email = formData.get('email') as string
  const password = formData.get('password') as string

  const { error } = await supabase.auth.signInWithPassword({
    email,
    password,
  })

  if (error) {
    // В реальном проекте возвращаем state с ошибкой
    redirect('/error')
  }

  revalidatePath('/', 'layout')
  redirect('/dashboard')
}

Шаг 3: OAuth Callback (Для Google/GitHub)

Если вы используете социальный вход или Magic Links, Supabase вернет пользователя на ваш сайт с кодом в URL. Нам нужен роут, который поймает этот код и обменяет на сессию.

src/app/auth/callback/route.ts:

TYPESCRIPT
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
  const next = searchParams.get('next') ?? '/dashboard'

  if (code) {
    const supabase = createClient()
    // Обмениваем одноразовый код на сессию
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    
    if (!error) {
      return NextResponse.redirect(`${origin}${next}`)
    }
  }

  // Если ошибка или нет кода
  return NextResponse.redirect(`${origin}/auth/auth-code-error`)
}

3. Подводные камни

  1. Кэширование юзера:

Next.js агрессивно кэширует всё. Если вы в лейауте получили юзера, а потом сделали логаут, Next может показать старый хедер.
Решение: Используйте revalidatePath при логине/логауте.

  1. Защита только на клиенте:

Никогда не полагайтесь на проверку if (!user) redirect() внутри useEffect. Это "дырявая" защита. Данные начнут грузиться до редиректа. Защита должна быть на уровне Middleware или в корне Server Page.

  1. Confirm Email:

По умолчанию Supabase требует подтверждения почты. Если вы это не отключили и пытаетесь залогиниться сразу после регистрации — получите ошибку Email not confirmed. Для дева отключайте, для прода — настраивайте красивые шаблоны писем.



4. Итоговое мнение

Связка Next.js Middleware + Supabase Auth — это золотой стандарт на 2025 год.

  • Плюсы: Безопасно (HttpOnly куки не украсть через XSS), работает с SSR, нативная поддержка TypeScript.

  • Минусы: Middleware может быть сложным в отладке.

Вы получаете систему авторизации enterprise-уровня (MFA, SSO, Social Login) за 0 рублей и 30 минут настройки. Писать свою auth-систему с нуля сейчас — это либо безумие, либо учебная задача. В продакшене так не делают.

Нужен похожий проект?

Свяжитесь с нами для бесплатной консультации.

Обсудить задачу
Безопасная аутентификация в Next.js (SSR & Middleware) | Блог ShmonovStudio