В современных веб-приложениях на Next.js контроль доступа к маршрутам чаще всего строится вокруг JSON Web Token (JWT). Этот механизм позволяет хранить утверждения о пользователе в подписанном токене и проверять их на стороне сервера или middleware без обращения к базе данных при каждом запросе.
Библиотека jose реализует стандарт JOSE (JSON Object Signing and Encryption) и предоставляет инструменты для работы с JWT, JWS и JWE. В экосистеме Next.js она используется как один из наиболее безопасных и актуальных способов подписи и проверки токенов благодаря поддержке современных криптографических алгоритмов и строгой реализации спецификаций.
Защита маршрутов в Next.js обычно строится по следующей схеме:
Ключевой элемент этой схемы — корректная проверка подписи токена. Именно здесь используется jose.
Для работы с JWT через JOSE в проекте Next.js устанавливается библиотека:
npm install jose
В приложениях на App Router дополнительно важно учитывать среду выполнения (Edge Runtime или Node.js), так как jose поддерживает обе, но требует корректного выбора API.
Для генерации токена используется 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 является симметричным и часто
используется для простых сценариевПроверка токена выполняется через 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
}
}
При ошибке подписи или истечении срока действия токен считается недействительным.
В 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*']
}
Механика работы:
В 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-слоя.
Для более сложных систем вместо 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)
}
Преимущество такого подхода:
Для хранения 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-атаки.
JWT обычно имеет ограниченный срок жизни, поэтому применяется схема refresh 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 проверка токена может выполняться напрямую:
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>
}
}
Такой подход исключает необходимость клиентских проверок доступа.
Вся система опирается на корректную работу подписи и проверки JWT через jose.