Шаблони аутентифікації в 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 захищають мутації
Швидкий приклад
// 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()
Таблиця порівняння
| Шар | Де виконується | Запити до БД | Коли використовувати |
|---|---|---|---|
| Middleware | Edge (V8) | Ні | Широкий захист маршрутів |
| Server Component | Node.js (SSR) | Так | Аутентифікація + дані сторінки |
| Route Handler | Node.js | Так | Захист API |
| Server Action | Node.js | Так | Захист мутацій |
| Layout | Node.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
// Неправильно - аутентифікація запускається на кожному 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 при редиректі
// Неправильно - користувач втрачає пункт призначення після входу
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
// Неправильно - 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!));Помилка: перевірка аутентифікації тільки на клієнті
// Неправильно (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
// 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 із перевіркою і отриманням даних за один прохід
// 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
// 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 із перевіркою авторизації
"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() на початку будь-який запит, що має вигляд валідного виклику, може запустити мутацію.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.