Backend

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

Пошаговая настройка 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 (

);
}

Анализ безопасности и оптимизация

Разберем, почему эта схема считается надежной и где ее можно улучшить:

  1. XSS-защита: Благодаря HTTP-only cookies, вредоносный скрипт не может украсть токен.
  2. CSRF-защита: Next.js Server Actions имеют встроенную защиту от CSRF-атак.
  3. 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-провайдерами или сложной системой управления сессиями.