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). Звучит страшно, но суть проста:
- Клиент просит ссылку на логин.
- Supabase возвращает одноразовый
code.
- Сервер Next.js меняет этот
codeнаsession(access + refresh token) и записывает их в куки.
Шаг 1: Middleware (Страж ворот)
Это самый критичный кусок кода. Middleware в Next.js запускается перед каждым запросом. Его задача — обновить сессию (refresh token), если она протухла, и передать обновленные куки дальше.
src/middleware.ts:
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:
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:
'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:
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. Подводные камни
- Кэширование юзера:
Next.js агрессивно кэширует всё. Если вы в лейауте получили юзера, а потом сделали логаут, Next может показать старый хедер.
Решение: Используйте revalidatePath при логине/логауте.
- Защита только на клиенте:
Никогда не полагайтесь на проверку if (!user) redirect() внутри useEffect. Это "дырявая" защита. Данные начнут грузиться до редиректа. Защита должна быть на уровне Middleware или в корне Server Page.
- 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-систему с нуля сейчас — это либо безумие, либо учебная задача. В продакшене так не делают.
Нужен похожий проект?
Свяжитесь с нами для бесплатной консультации.
