Next.js: защита маршрутов через JWT

В современных веб-приложениях на Next.js контроль доступа к маршрутам чаще всего строится вокруг JSON Web Token (JWT). Этот механизм позволяет хранить утверждения о пользователе в подписанном токене и проверять их на стороне сервера или middleware без обращения к базе данных при каждом запросе.

Библиотека jose реализует стандарт JOSE (JSON Object Signing and Encryption) и предоставляет инструменты для работы с JWT, JWS и JWE. В экосистеме Next.js она используется как один из наиболее безопасных и актуальных способов подписи и проверки токенов благодаря поддержке современных криптографических алгоритмов и строгой реализации спецификаций.


Базовая модель защиты маршрутов через JWT

Защита маршрутов в Next.js обычно строится по следующей схеме:

  • пользователь проходит аутентификацию
  • сервер формирует JWT и подписывает его
  • токен сохраняется в cookie или передаётся в заголовке Authorization
  • при каждом запросе Next.js проверяет токен
  • доступ к маршруту разрешается или запрещается

Ключевой элемент этой схемы — корректная проверка подписи токена. Именно здесь используется jose.


Установка и подготовка

Для работы с JWT через JOSE в проекте Next.js устанавливается библиотека:

npm install jose

В приложениях на App Router дополнительно важно учитывать среду выполнения (Edge Runtime или Node.js), так как jose поддерживает обе, но требует корректного выбора API.


Создание и подпись JWT

Для генерации токена используется SignJWT.

Пример создания токена после логина:

import { SignJWT } from 'jose'

const secret = new TextEncoder().encode(process.env.JWT_SECRET)

export async function createToken(user) {
  return await new SignJWT({
    sub: user.id,
    role: user.role
  })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('2h')
    .sign(secret)
}

Ключевые моменты:

  • sub используется для идентификации пользователя
  • setIssuedAt() фиксирует время создания
  • setExpirationTime() ограничивает срок жизни токена
  • алгоритм HS256 является симметричным и часто используется для простых сценариев

Проверка JWT в Next.js

Проверка токена выполняется через jwtVerify.

import { jwtVerify } from 'jose'

const secret = new TextEncoder().encode(process.env.JWT_SECRET)

export async function verifyToken(token) {
  try {
    const { payload } = await jwtVerify(token, secret)
    return payload
  } catch (error) {
    return null
  }
}

При ошибке подписи или истечении срока действия токен считается недействительным.


Защита маршрутов через middleware

В Next.js App Router наиболее распространённый способ защиты — middleware.

import { NextResponse } from 'next/server'
import { jwtVerify } from 'jose'

const secret = new TextEncoder().encode(process.env.JWT_SECRET)

export async function middleware(request) {
  const token = request.cookies.get('token')?.value

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  try {
    await jwtVerify(token, secret)
    return NextResponse.next()
  } catch {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*']
}

Механика работы:

  • middleware перехватывает запрос
  • извлекает JWT из cookie
  • проверяет его через jose
  • перенаправляет при ошибке
  • пропускает запрос при успехе

Защита API маршрутов

В API Routes или Route Handlers проверка выполняется аналогично.

import { jwtVerify } from 'jose'

const secret = new TextEncoder().encode(process.env.JWT_SECRET)

export async function GET(request) {
  const token = request.headers.get('authorization')?.split(' ')[1]

  if (!token) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  try {
    const { payload } = await jwtVerify(token, secret)

    return Response.json({
      message: 'Доступ разрешён',
      user: payload.sub
    })
  } catch {
    return Response.json({ error: 'Invalid token' }, { status: 401 })
  }
}

Такой подход обеспечивает единообразную защиту API-слоя.


Использование RSA и асимметричных ключей

Для более сложных систем вместо HS256 применяется RSA (RS256), где подпись создаётся приватным ключом, а проверка выполняется публичным.

import { SignJWT } from 'jose'
import { readFileSync } from 'fs'

const privateKey = readFileSync('./private.pem')

export async function createToken(user) {
  return await new SignJWT({ sub: user.id })
    .setProtectedHeader({ alg: 'RS256' })
    .setIssuedAt()
    .setExpirationTime('1h')
    .sign(privateKey)
}

Преимущество такого подхода:

  • приватный ключ хранится только на сервере авторизации
  • проверяющие сервисы используют только публичный ключ
  • повышается безопасность при распределённой архитектуре

Работа с cookies в Next.js

Для хранения JWT часто используются HTTP-only cookies:

import { cookies } from 'next/headers'

export async function setAuthCookie(token) {
  cookies().set('token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    path: '/'
  })
}

Это снижает риск кражи токена через XSS-атаки.


Обновление токенов (refresh strategy)

JWT обычно имеет ограниченный срок жизни, поэтому применяется схема refresh token.

Логика:

  • access token живёт короткое время
  • refresh token живёт дольше
  • при истечении access token запрашивается новый

Пример проверки refresh:

export async function refreshAccessToken(refreshToken) {
  const { payload } = await jwtVerify(refreshToken, secret)

  if (!payload) return null

  return await createToken({
    id: payload.sub,
    role: payload.role
  })
}

Частые ошибки при интеграции Jose в Next.js

  • использование одинакового срока жизни для access и refresh токенов
  • хранение JWT в localStorage вместо httpOnly cookie
  • отсутствие проверки алгоритма подписи
  • смешивание Edge Runtime и Node.js APIs без адаптации
  • отсутствие обработки исключений jwtVerify

Безопасность и контроль целостности

При использовании jose важно учитывать несколько аспектов:

  • всегда фиксировать алгоритм в заголовке токена
  • не доверять payload без проверки подписи
  • ограничивать время жизни токена
  • использовать разные ключи для разных окружений
  • регулярно ротировать секреты

Интеграция с App Router и серверными компонентами

В серверных компонентах Next.js проверка токена может выполняться напрямую:

import { cookies } from 'next/headers'
import { jwtVerify } from 'jose'

export default async function DashboardPage() {
  const token = cookies().get('token')?.value

  if (!token) {
    return <div>Нет доступа</div>
  }

  try {
    const { payload } = await jwtVerify(
      token,
      new TextEncoder().encode(process.env.JWT_SECRET)
    )

    return <div>Пользователь: {payload.sub}</div>
  } catch {
    return <div>Ошибка авторизации</div>
  }
}

Такой подход исключает необходимость клиентских проверок доступа.


Структура типичной системы авторизации

  • Auth Service: выдача JWT
  • Next.js Middleware: первичная проверка маршрутов
  • API Routes: проверка доступа к данным
  • Server Components: защита UI-уровня
  • Cookies: безопасное хранение токенов

Вся система опирается на корректную работу подписи и проверки JWT через jose.