Skip to main content

Шаблони аутентифікації в Next.js

Шаблони аутентифікації в Next.js - це підходи до перевірки особи користувача на різних етапах обробки запиту: на edge (Middleware), під час рендерингу сторінки (Server Components), на рівні API (Route Handlers) та перед мутаціями (Server Actions).

Теорія

TL;DR

  • Уяви аеропорт: Middleware перевіряє всіх на вході до зони вильоту (кожен запит), Server Components перевіряють посадковий талон біля виходу на борт (рендеринг сторінки), Route Handlers перевіряють на кожній стійці реєстрації (API-виклик)
  • Middleware найшвидший шар, але найбільш обмежений: без запитів до БД, без Node.js-модулів
  • Server Components перевіряють авторизацію і отримують дані за один SSR-прохід, без зайвого клієнтського запиту
  • Route Handlers і Server Actions - це публічні ендпоінти, кожен потребує власного явного виклику getSession()
  • Комбінуй шари: Middleware блокує рано, Server Components отримують дані, Server Actions захищають мутації

Швидкий приклад

tsx
// middleware.ts - захищає /dashboard/* на рівні edge import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; const publicPaths = ["/", "/login", "/register", "/api/auth"]; export function middleware(request: NextRequest) { const { pathname } = request.nextUrl; if (publicPaths.some(path => pathname.startsWith(path))) { return NextResponse.next(); // публічні шляхи пропускаємо } const token = request.cookies.get("auth-token")?.value; if (!token) { const loginUrl = new URL("/login", request.url); loginUrl.searchParams.set("callbackUrl", pathname); // зберігаємо пункт призначення return NextResponse.redirect(loginUrl); } return NextResponse.next(); } export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], };

Middleware виконується у Edge Runtime від Vercel (V8 ізоляти), без повноцінного Node.js. Він читає заголовки і cookies через NextRequest і повертає відповідь до того, як запит дійде до твоїх сторінок.

Головна різниця

Middleware перехоплює запити до того, як запустився будь-який код сторінки. Це швидко, але є обмеження: немає Node.js API і прямих запитів до БД. Server Components виконуються під час SSR у Node.js і можуть звертатися до БД напряму, але лише при рендерингу конкретної сторінки. Route Handlers - це охорона на рівні окремого ендпоінта. Server Actions теж є публічними мережевими ендпоінтами і потребують власних перевірок, бо їх можна викликати напряму з клієнта.

Коли що використовувати

  • Захист цілих розділів (адмінка, дашборд) - Middleware
  • Отримання даних користувача на конкретній сторінці - Server Component
  • Захист API-ендпоінтів - Route Handler
  • Валідація перед записом у БД - Server Action
  • OAuth (Google, GitHub) - Route Handler через NextAuth.js
  • Рольовий доступ на рівні сторінки - Server Component із notFound() або redirect()

Таблиця порівняння

ШарДе виконуєтьсяЗапити до БДКоли використовувати
MiddlewareEdge (V8)НіШирокий захист маршрутів
Server ComponentNode.js (SSR)ТакАутентифікація + дані сторінки
Route HandlerNode.jsТакЗахист API
Server ActionNode.jsТакЗахист мутацій
LayoutNode.js (SSR)ТакАутентифікація розділу

Як це працює всередині

Middleware запускається у Edge Runtime до маршрутизації. Він має доступ до NextRequest (заголовки, cookies, URL), але не може використовувати Node.js-модулі на кшталт pg, fs чи Prisma-клієнта. Server Components виконуються в Node.js під час SSR і отримують cookies через cookies() з next/headers. Авторизаційний стан вбудовується прямо в RSC payload, тому окремого запиту з клієнта не потрібно.

Одна річ, яку я бачив у кількох командах: Middleware запускається на кожному відповідному запиті, включно з API-маршрутами. Якщо matcher занадто широкий, edge-затримка з'являється навіть на health check.

Типові помилки

Помилка: статичні ресурси потрапляють у Middleware

tsx
// Неправильно - аутентифікація запускається на кожному CSS, JS, зображенні export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], }; // Краще - захищай лише те, що потрібно export const config = { matcher: ["/dashboard/:path*", "/profile/:path*", "/admin/:path*"], };

Широкий negative-lookahead-матчер є в документації Next.js для i18n-сценаріїв. Для аутентифікації явне перерахування шляхів уникає зайвих edge-витрат.

Помилка: немає callbackUrl при редиректі

tsx
// Неправильно - користувач втрачає пункт призначення після входу return NextResponse.redirect(new URL("/login", req.url)); // Правильно - після входу повертаємо туди, куди йшли const loginUrl = new URL("/login", req.url); loginUrl.searchParams.set("callbackUrl", req.nextUrl.pathname); return NextResponse.redirect(loginUrl);

Помилка: запити до БД у Middleware

tsx
// Неправильно - Prisma, pg, Drizzle не працюють у edge runtime const user = await prisma.user.findUnique({ where: { id: session.sub } }); // Правильно - використовуй jose для перевірки JWT на edge import { jwtVerify } from "jose"; await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET!));

Помилка: перевірка аутентифікації тільки на клієнті

tsx
// Неправильно (Client Component) - користувач може підробити це "use client"; if (!document.cookie.includes("auth-token")) router.push("/login"); // Правильно - перевіряй у Server Component або Middleware const session = await getSession(); // лише на сервері if (!session) redirect("/login");

Помилка: пропущена перевірка у Server Actions

Server Actions - це публічні ендпоінти. Виклик updateProfile() з клієнта проходить через ту саму мережу, що й будь-який API-маршрут. Завжди викликай getSession() на початку кожного Server Action, що торкається даних користувача. Ніколи не пропускай цей крок мовчки.

Реальне використання

  • NextAuth.js (Auth.js v5) - Middleware і Route Handlers для OAuth, хелпер auth() у Server Components
  • Clerk - Middleware для рольового доступу, поширений у T3 Stack
  • Supabase - Server Components із createServerClient() для row-level security
  • Lucia - кастомні сесії через Route Handlers у open-source стартерах
  • Власний JWT - Middleware з бібліотекою jose для перевірки на edge

Follow-up питання

Q: Чим відрізняється Middleware від getSession() у Server Component?
A: Middleware виконується на edge до завантаження сторінки і не має доступу до Node.js-модулів. getSession() у Server Component виконується в Node.js під час SSR і може звертатися до БД. Middleware блокує, Server Component отримує дані.

Q: Що відбувається зі статичними сторінками при Middleware-аутентифікації?
A: Статичні сторінки обходять Middleware, якщо не потрапляють у matcher. Завжди виключай /_next/static і /_next/image з auth-матчерів, інакше кожен запит статичного ресурсу йде через edge.

Q: Як реалізувати рольовий доступ у Server Components?
A: Отримай сесію, перевір session.user.role, потім викличи redirect() або notFound(). Наприклад, if (session.role !== "admin") notFound() повертає 404 замість того, щоб розкрити факт існування сторінки.

Q: Чому не можна виконувати запити до БД у Middleware?
A: Edge Runtime працює у V8 ізолятах без Node.js API. pg, Prisma і Drizzle залежать від Node-модулів, яких на edge немає. Використовуй JWT через jose або зовнішні сервіси на кшталт Upstash Redis.

Q: Як Middleware взаємодіє з parallel routes в App Router?
A: Middleware виконується до будь-яких intercepting routes. Parallel routes успадковують сесію від батьківського Server Component у layout. З хелпером auth() від Auth.js v5 сесію отримують один раз у layout і передають у дочірні слоти, уникаючи дублювання запитів.

Q: Як перенести аутентифікацію з Pages Router до App Router?
A: Замінюй логіку _app.getInitialProps на Middleware. Перевірки сесій у getServerSideProps переносяться до Server Components із cookies() з next/headers. Патерн getServerSession() відповідає auth() у Auth.js v5.

Приклади

Middleware із збереженням callbackUrl

tsx
// middleware.ts import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; export function middleware(request: NextRequest) { const token = request.cookies.get("auth-token")?.value; const isLoginPage = request.nextUrl.pathname.startsWith("/login"); if (!token && !isLoginPage) { const loginUrl = new URL("/login", request.url); loginUrl.searchParams.set("callbackUrl", request.nextUrl.pathname); return NextResponse.redirect(loginUrl); } return NextResponse.next(); } export const config = { matcher: ["/dashboard/:path*", "/profile/:path*", "/admin/:path*"], };

Явний matcher захищає три розділи без зайвих витрат на статичні ресурси. Після входу застосунок читає callbackUrl із query string і повертає користувача туди, куди він хотів потрапити.

Server Component із перевіркою і отриманням даних за один прохід

tsx
// lib/auth.ts import { cookies } from "next/headers"; export async function getSession() { const cookieStore = await cookies(); const token = cookieStore.get("auth-token")?.value; if (!token) return null; try { return await verifyToken(token); // повертає об'єкт користувача } catch { return null; } } // app/dashboard/page.tsx import { redirect } from "next/navigation"; import { getSession } from "@/lib/auth"; export default async function DashboardPage() { const session = await getSession(); if (!session) redirect("/login"); const stats = await fetchUserStats(session.user.id); // один прохід, без зайвого запиту return ( <div> <h1>Ласкаво просимо, {session.user.name}</h1> <StatsPanel data={stats} /> </div> ); }

Server Component перевіряє авторизацію і отримує дані сторінки за один прохід. Жодного клієнтського round-trip, жодного стану завантаження для auth-перевірки.

NextAuth.js v5 із власним Middleware

tsx
// auth.ts import NextAuth from "next-auth"; import GitHub from "next-auth/providers/github"; import Credentials from "next-auth/providers/credentials"; export const { auth, handlers, signIn, signOut } = NextAuth({ providers: [ GitHub, Credentials({ credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" }, }, authorize: async (credentials) => { const user = await getUserByEmail(credentials.email as string); if (!user) return null; const valid = await verifyPassword(credentials.password as string, user.password); return valid ? user : null; }, }), ], }); // app/api/auth/[...nextauth]/route.ts import { handlers } from "@/auth"; export const { GET, POST } = handlers; // middleware.ts - Auth.js v5 надає власну Middleware-функцію export { auth as middleware } from "@/auth"; export const config = { matcher: ["/dashboard/:path*"], };

Auth.js v5 експортує Middleware-функцію, яку можна використовувати напряму або обгорнути для власної логіки. Хелпер auth() однаково працює в Server Components і Server Actions без додаткового налаштування.

Server Action із перевіркою авторизації

tsx
"use server"; import { getSession } from "@/lib/auth"; import { revalidatePath } from "next/cache"; export async function updateProfile(formData: FormData) { const session = await getSession(); if (!session) { throw new Error("Unauthorized"); // ніколи не мовчи при помилці } const name = formData.get("name") as string; await db.update(users) .set({ name }) .where(eq(users.id, session.user.id)); revalidatePath("/profile"); }

Server Actions - це публічні мережеві ендпоінти. Без перевірки getSession() на початку будь-який запит, що має вигляд валідного виклику, може запустити мутацію.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?