Пошаговая настройка JWT авторизации в Next.js 14 App Router

В современном веб-разработке выбор способа аутентификации определяет не только безопасность приложения, но и его масштабируемость. С выходом Next.js 14 и развитием App Router, подход к управлению сессиями изменился. Мы перешли от клиентского рендеринга к серверным компонентам (Server Components), что заставляет нас переосмыслить хранение токенов и проверку прав доступа.
В этой статье мы разберем, как реализовать полноценную JWT-авторизацию, используя HTTP-only cookies, Middleware и серверные экшены.
Почему именно JWT и почему HTTP-only Cookies?
Перед тем как переходить к коду, определимся со стеком. JWT (JSON Web Token) идеален для распределенных систем, так как серверу не нужно хранить сессию в базе данных или Redis — вся информация содержится в самом токене.
Однако хранение JWT в localStorage — это критическая ошибка безопасности. XSS-атаки позволяют злоумышленникам легко украсть токен. Поэтому мы будем использовать HTTP-only Cookies. Такие куки недоступны для JavaScript на стороне клиента, что делает кражу токена практически невозможной.
Шаг 1: Подготовка окружения и установка зависимостей
Для реализации нам понадобятся две основные библиотеки: jose для работы с JWT (она легкая и работает в Edge Runtime, что критично для Middleware) и bcryptjs для хеширования паролей.
bash
npm install jose bcryptjs
npm install -D @types/bcryptjs
В файле .env.local создадим секретный ключ. Важно, чтобы он был достаточно длинным и случайным:
env
JWT_SECRET=your_super_secret_random_string_at_least_32_chars
Шаг 2: Создание утилиты для работы с токенами
В App Router логика работы с токенами должна быть вынесена в отдельные серверные утилиты. Мы создадим файл lib/auth.ts, который будет отвечать за подпись и верификацию JWT.
typescript
import { SignJWT, jwtVerify } from ‘jose’;
import { cookies } from ‘next/headers’;
import { NextRequest, NextResponse } from ‘next/server’;
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
export async function encrypt(payload: any) {
return await new SignJWT(payload)
.setProtectedHeader({ alg: ‘HS256’ })
.setIssuedAt()
.setExpirationTime(‘2h’) // Срок жизни токена — 2 часа
.sign(SECRET);
}
export async function decrypt(input: string): Promise {
const { payload } = await jwtVerify(input, SECRET, {
algorithms: [‘HS256’],
});
return payload;
}
Здесь мы используем библиотеку jose, так как стандартная jsonwebtoken не работает в Edge Runtime (на котором работает Middleware в Next.js).
Шаг 3: Реализация Server Actions для регистрации и входа
В Next.js 14 Server Actions позволяют обрабатывать формы без создания отдельных API-роутов. Это сокращает количество бойлерплейта и улучшает типизацию.
Создадим файл app/actions/auth.ts:
typescript
‘use server’;
import { encrypt } from ‘@/lib/auth’;
import { cookies } from ‘next/headers’;
import bcrypt from ‘bcryptjs’;
import { redirect } from ‘next/navigation’;
// Имитация базы данных (в реальном проекте замените на Prisma/Mongoose)
const usersDb = [];
export async function register(formData: FormData) {
const email = formData.get(’email’) as string;
const password = formData.get(‘password’) as string;
const hashedPassword = await bcrypt.hash(password, 10);
usersDb.push({ email, password: hashedPassword });
redirect(‘/login’);
}
export async function login(formData: FormData) {
const email = formData.get(’email’) as string;
const password = formData.get(‘password’) as string;
const user = usersDb.find((u) => u.email === email);
if (!user || !(await bcrypt.compare(password, user.password))) {
return { error: ‘Неверный логин или пароль’ };
}
// Создаем токен
const token = await encrypt({ email: user.email });
// Устанавливаем cookie
cookies().set(‘session’, token, {
httpOnly: true,
secure: process.env.NODE_ENV === ‘production’,
sameSite: ‘lax’,
path: ‘/’,
});
redirect(‘/dashboard’);
}
export async function logout() {
cookies().delete(‘session’);
redirect(‘/login’);
}
Важный нюанс: Обратите внимание на настройки cookies().set. Параметр httpOnly: true запрещает доступ к куке через document.cookie, а secure: true гарантирует передачу только по HTTPS.
Шаг 4: Защита роутов через Middleware
Чтобы пользователь не мог попасть на страницу /dashboard без авторизации, мы настроим middleware.ts в корне проекта. Это «фильтр», который перехватывает запросы до того, как они попадут в рендеринг страницы.
typescript
import { NextRequest, NextResponse } from ‘next/server’;
import { decrypt } from ‘@/lib/auth’;
export async function middleware(request: NextRequest) {
const session = request.cookies.get(‘session’)?.value;
// Список путей, которые требуют авторизации
const isProtectedRoute = request.nextUrl.pathname.startsWith(‘/dashboard’);
// Список путей, куда нельзя заходить авторизованным (например, /login)
const isAuthRoute = request.nextUrl.pathname.startsWith(‘/login’) ||
request.nextUrl.pathname.startsWith(‘/register’);
if (isProtectedRoute) {
if (!session) {
return NextResponse.redirect(new URL(‘/login’, request.url));
}
try {
await decrypt(session);
return NextResponse.next();
} catch (e) {
return NextResponse.redirect(new URL(‘/login’, request.url));
}
}
if (isAuthRoute && session) {
return NextResponse.redirect(new URL(‘/dashboard’, request.url));
}
return NextResponse.next();
}
export const config = {
matcher: [‘/dashboard/:path*’, ‘/login’, ‘/register’],
};
Этот механизм позволяет централизованно управлять доступом, не прописывая проверки на каждой отдельной странице.
Шаг 5: Создание интерфейса (Frontend)
Теперь создадим простую форму входа app/login/page.tsx. Поскольку мы используем Server Actions, нам не нужно писать fetch запросы.
tsx
import { login } from ‘@/app/actions/auth’;
export default function LoginPage() {
return (
);
}
Шаг 6: Получение данных пользователя на сервере
В App Router мы можем получать данные пользователя прямо в серверном компоненте. Это избавляет нас от «мерцания» интерфейса (Layout Shift), которое часто бывает при проверке авторизации на клиенте.
Пример в app/dashboard/page.tsx:
tsx
import { decrypt } from ‘@/lib/auth’;
import { cookies } from ‘next/headers’;
import { logout } from ‘@/app/actions/auth’;
export default async function DashboardPage() {
const session = cookies().get(‘session’)?.value;
const user = session ? await decrypt(session) : null;
return (
);
}
Анализ безопасности и оптимизация
Разберем, почему эта схема считается надежной и где ее можно улучшить:
- XSS-защита: Благодаря HTTP-only cookies, вредоносный скрипт не может украсть токен.
- CSRF-защита: Next.js Server Actions имеют встроенную защиту от CSRF-атак.
- Edge Runtime: Использование
joseпозволяет проверять токены на границе сети (Edge), что значительно ускоряет время отклика (TTFB).
Как улучшить систему в продакшене?
-
- Refresh Tokens: В данной реализации токен живет 2 часа. Для реального проекта стоит внедрить механизм Refresh-токенов, которые хранятся в БД и позволяют обновлять Access-токен без повторного ввода пароля.
-
- Валидация данных: Используйте библиотеку
zodдля валидации входящих данных в Server Actions, чтобы избежать инъекций и некорректного ввода.
- Валидация данных: Используйте библиотеку
-
- Обработка ошибок: Вместо простого
redirectв Server Actions, используйтеuseFormState(в клиентских компонентах) для отображения конкретных ошибок пользователю.
- Обработка ошибок: Вместо простого
Заключение
Реализация JWT-авторизации в Next.js 14 через App Router смещает фокус с клиентской логики на серверную. Мы ушли от использования useEffect и useState для проверки сессии, заменив их на Middleware и серверные компоненты. Это не только повышает безопасность, но и существенно улучшает SEO и UX за счет отсутствия задержек при загрузке защищенных страниц.
Данный подход является фундаментом, на котором можно строить сложные системы с разграничением прав доступа (RBAC), интеграцией с внешними OAuth-провайдерами или сложной системой управления сессиями.

