Аутентификация в Next.js с JWT: пошаговое руководство
О чём этот материал
- Основной подход: выдавать краткоживущий access token (JWT) при логине и хранить его в защищённой куки.
- Проверять токен на сервере (middleware и API-эндпоинты) с помощью библиотеки jose.
- Обеспечить UX: перенаправления, состояние клиента и поддержка защищённых маршрутов.
Варианты, которые обсуждаются: NextAuth (готовое решение), собственная реализация JWT, использование refresh-токенов, хранение токенов в куках против localStorage.
Ключевые термины — одно предложение каждая
- JWT: JSON Web Token — компактный самодостаточный токен с полезной нагрузкой и подписью.
- Access token: короткоживущий JWT, подтверждающий права доступа.
- Refresh token: долгоживущий токен для получения новых access token.
- Cookie (куки): механизм хранения данных в браузере, может быть защищён флагами HttpOnly и Secure.
Предварительные требования
- Node.js и npm установлены.
- Проект на Next.js 13 с директориeй app.
- Понимание основ fetch API, React hooks и работы с куками.
Установка и инициализация проекта
Чтобы начать, создайте проект Next.js 13 (app directory):
npx create-next-app@latest next-auth-jwt --experimental-appУстановите зависимости:
npm install jose universal-cookieJose — утилиты для работы с JWT. Universal-cookie — удобная библиотека для работы с куками как на клиенте, так и на сервере.
Код примера доступен в репозитории (упомянуто в оригинале).
UI: форма входа
Создайте страницу логина в src/app/login/page.js со следующим компонентом (клиентский компонент):
"use client";
import { useRouter } from "next/navigation";
export default function LoginPage() {
return (
);
}Этот компонент рендерит простую форму. Директива “use client” объявляет, что код выполняется в браузере.
Добавим обработчик отправки формы внутрь компонента:
const router = useRouter();
const handleSubmit = async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const username = formData.get("username");
const password = formData.get("password");
const res = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ username, password }),
});
const { success } = await res.json();
if (success) {
router.push("/protected");
router.refresh();
} else {
alert("Login failed");
}
};Этот хендлер отправляет POST на /api/login, ожидая, что сервер установит куку с JWT при удачном входе.
API-эндпоинт логина — генерация JWT
Создайте src/app/api/login/route.js и вставьте следующий код:
import { SignJWT } from "jose";
import { NextResponse } from "next/server";
import { getJwtSecretKey } from "@/libs/auth";
export async function POST(request) {
const body = await request.json();
if (body.username === "admin" && body.password === "admin") {
const token = await new SignJWT({
username: body.username,
})
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("30s")
.sign(getJwtSecretKey());
const response = NextResponse.json(
{ success: true },
{ status: 200, headers: { "content-type": "application/json" } }
);
response.cookies.set({
name: "token",
value: token,
path: "/",
});
return response;
}
return NextResponse.json({ success: false });
}Комментарий: Здесь используется простая заглушка: логин успешен только для admin/admin. При успешном логине сервер подписывает JWT и возвращает его в куке “token”.
Важно: у вас в продакшене кука должна иметь флаги HttpOnly, Secure и SameSite, чтобы повысить безопасность (см. раздел “Усиление безопасности”).
Проверка JWT
Создайте файл src/libs/auth.js со следующей логикой проверки:
import { jwtVerify } from "jose";
export function getJwtSecretKey() {
const secret = process.env.NEXT_PUBLIC_JWT_SECRET_KEY;
if (!secret) {
throw new Error("JWT Secret key is not matched");
}
return new TextEncoder().encode(secret);
}
export async function verifyJwtToken(token) {
try {
const { payload } = await jwtVerify(token, getJwtSecretKey());
return payload;
} catch (error) {
return null;
}
}Добавьте в корень проекта .env:
NEXT_PUBLIC_JWT_SECRET_KEY=your_secret_keyПримечание: в продакшне секрет должен храниться в безопасном месте (секреты окружения, vault) и не попадать в клиентский бандл.
Защищённая страница
Создайте src/app/protected/page.js:
export default function ProtectedPage() {
return Very protected page
;
}Вместо простого заголовка разместите реальное защищённое содержимое или SSR-логики, проверяющие права доступа.
Клиентский хук для состояния аутентификации
src/hooks/useAuth/index.js:
"use client" ;
import React from "react";
import Cookies from "universal-cookie";
import { verifyJwtToken } from "@/libs/auth";
export function useAuth() {
const [auth, setAuth] = React.useState(null);
const getVerifiedtoken = async () => {
const cookies = new Cookies();
const token = cookies.get("token") ?? null;
const verifiedToken = await verifyJwtToken(token);
setAuth(verifiedToken);
};
React.useEffect(() => {
getVerifiedtoken();
}, []);
return auth;
}Хук читает куку и вызывает verifyJwtToken. Это даёт клиентскому UI информацию о текущем пользователе.
В app/page.js пример использования:
"use client" ;
import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
export defaultfunction Home() {
const auth = useAuth();
return <>
Public Home Page
>
}Middleware для защиты маршрутов
Создайте src/middleware.js с приведённой логикой проверки куки и перенаправления:
import { NextResponse } from "next/server";
import { verifyJwtToken } from "@/libs/auth";
const AUTH_PAGES = ["/login"];
const isAuthPages = (url) => AUTH_PAGES.some((page) => page.startsWith(url));
export async function middleware(request) {
const { url, nextUrl, cookies } = request;
const { value: token } = cookies.get("token") ?? { value: null };
const hasVerifiedToken = token && (await verifyJwtToken(token));
const isAuthPageRequested = isAuthPages(nextUrl.pathname);
if (isAuthPageRequested) {
if (!hasVerifiedToken) {
const response = NextResponse.next();
response.cookies.delete("token");
return response;
}
const response = NextResponse.redirect(new URL(`/`, url));
return response;
}
if (!hasVerifiedToken) {
const searchParams = new URLSearchParams(nextUrl.searchParams);
searchParams.set("next", nextUrl.pathname);
const response = NextResponse.redirect(
new URL(`/login?${searchParams}`, url)
);
response.cookies.delete("token");
return response;
}
return NextResponse.next();
}
export const config = { matcher: ["/login", "/protected/:path*"] };Middleware проверяет токен и перенаправляет неаутентифицированных пользователей на страницу логина, сохраняя путь в параметре next.
Усиление безопасности (важно)
Ниже — практические рекомендации, которые помогут снизить риски при использовании JWT в браузере.
- Хранение токенов: предпочтительнее — HttpOnly куки с флагами Secure и SameSite=Strict или Lax. Избегайте хранения access token в localStorage, если важна защита от XSS.
- Срок жизни access token: делайте коротким (например, минуты/несколько часов). Для долгоживущих сеансов используйте refresh token, хранимый в безопасной HttpOnly куке на серверной стороне.
- Подпись и алгоритм: используйте сильный секрет и алгоритмы (HS256 / RS256 в зависимости от архитектуры). Для RS* храните приватный ключ отдельно.
- Защита от CSRF: при использовании куков добавьте CSRF-токен либо используйте SameSite куки в комбинации с проверками Origin/Referer.
- Ротация и отзыв: реализуйте возможность отзыва токена (черный список) для критичных сценариев (утечка, принудительный выход).
- Логи и мониторинг: логируйте попытки аутентификации и аномалии (много неудачных входов, необычные IP).
Important: в примере выше кука устанавливается без флагов безопасности — настройте их в продакшн.
Альтернативные подходы
- NextAuth: кастомная и более функциональная библиотека для Next.js, поддерживает провайдеры OAuth, сессии, JWT, адаптеры баз данных. Хороша, если нужен быстрый релиз.
- Сессии на сервере: хранить сессии в базе/Redis и обслуживать по session cookie — удобно для мгновенного отзыва прав.
- OAuth2 / OpenID Connect: для интеграции с внешними провайдерами идентификации.
Когда собственный JWT оправдан: простые API, микросервисы, необходимость контролировать payload токена. Когда не оправдан: если нужен быстрый старт с множеством провайдеров — лучше NextAuth.
Модель принятия решений (мнемоника)
- Если нужна мгновенная отзывчивость прав => сессии на сервере.
- Если нужна независимость сервисов и самодостаточные токены => JWT.
- Если важна простота интеграции с OAuth-провайдерами => NextAuth.
Тесты и критерии приёмки
Критерии приёмки:
- Успешный вход под валидными учётными данными приводит к установке куки “token” и редиректу на защищённую страницу.
- Без токена доступ к /protected приводит к редиректу на /login с параметром next.
- Время жизни токена соблюдается (после истечения — доступ запрещён).
- Попытка доступа к /login будучи аутентифицированным — перенаправляет на главную.
Минимальные тесты (acceptance):
- TC1: Валидный логин -> кука установлена, /protected доступна.
- TC2: Невалидный логин -> сообщение об ошибке, кука не установлена.
- TC3: Удаление куки -> доступы к защищённым ресурсам запрещены.
- TC4: Попытка CSRF -> отрабатывает защита (в зависимости от выбранной схемы).
Сценарии отказа и откат
Когда может провалиться:
- Секрет JWT изменён или не совпадает с тем, что использовался для подписи — все токены станут недействительны.
- Истечение срока токена без механизма refresh -> UX-проблемы.
- Неправильно настроенные куки (нет HttpOnly/Secure/SameSite) -> повышенный риск XSS/CSRF.
Откат: при смене секретов — реализуйте стратегию ротации ключей: сначала принимайте старые и новые ключи, затем постепенно откатывайте старые.
Рекомендации по GDPR и приватности
- Не храните в JWT чувствительные персональные данные (PII) в явном виде. Если нужно — храните только идентификатор пользователя и извлекайте данные с сервера по запросу.
- Сроки хранения: объясните пользователю и храните только необходимое время.
- Предоставьте пользователю возможность удалить аккаунт и связанные с ним данные — это может потребовать удаления/отзыва токенов.
Чеклист по ролям
Для разработчика фронтенда:
- Проверить, что кука читается корректно в useAuth.
- Обработать состояние проседания токена (loading, expired).
Для бекенд-разработчика:
- Обеспечить подпись токенов и безопасное хранение секретов.
- Настроить флаги куки и CSRF-защиту или реализовать защитные заголовки.
Для DevOps:
- Хранить секреты в безопасном хранилище.
- Настроить HTTPS для домена и Secure-флаг куки.
Шаблон проверки безопасности (коротко)
- HTTPS включён: да/нет
- HttpOnly: да/нет
- Secure: да/нет
- SameSite: Lax/Strict
- Срок жизни access token: установлен
- Реализован refresh token: да/нет
Отладка — основные шаги
- Проверьте, пришла ли кука в ответе сервера (Network → Set-Cookie).
- Убедитесь, что кука содержит ожидаемый JWT и флаги.
- Попробуйте вручную распарсить/проверить токен с тем же секретом (локально) с помощью jose.
- Логи: вывод ошибок jwtVerify для понимания причины (expired, signature invalid и т.д.).
Краткая таблица “Когда это работает/когда нет”
- Работает: одностраничные приложения с простым API, требующим аутентификации.
- Неидеально: приложения, где нужно мгновенно отзывать доступы без хранения списков отзыва на сервере (в этом случае лучше сессии).
Глоссарий (1 строка каждый)
- Access token: короткоживущий токен для авторизации запросов.
- Refresh token: долгоживущий токен для обновления access token.
- HttpOnly: атрибут куки, недоступный через JavaScript.
- SameSite: атрибут куки, ограничивающий отправку в кросс-сайтовых запросах.
Краткое резюме
- Собственная JWT-авторизация в Next.js даёт контроль, но требует аккуратной настройки безопасности.
- Используйте HttpOnly и Secure куки, короткие access token и опциональные refresh token’ы.
- Рассмотрите NextAuth или серверные сессии, если нужны готовые сценарии управления сессиями и авторизацией.
Дополнительные материалы: реализация refresh token’ов, ротация ключей, интеграция с внешними провайдерами — логичное продолжение этого руководства.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone