В спецификации JWT (JSON Web Token) существует стандартное поле
jti (JWT ID), предназначенное для уникальной идентификации
конкретного токена. Это значение играет ключевую роль в сценариях, где
требуется контроль над повторным использованием токенов и их жизненным
циклом.
Поле jti представляет собой строку, которая должна быть
уникальной в пределах системы. Чаще всего оно формируется как
криптографически стойкий идентификатор:
Основная задача jti — позволить серверу отличать один
токен от другого даже при совпадении остальных claims (sub, aud, exp и
т.д.).
Replay-атака возникает в момент, когда валидный JWT перехватывается и используется повторно без согласия пользователя. Поскольку JWT по своей природе является самодостаточным и не требует хранения на сервере, его повторное предъявление часто невозможно отличить от оригинального запроса.
Сценарии возникновения:
Особенно критичны ситуации, когда JWT используется как bearer-токен без дополнительной проверки состояния.
jti в защите от повторного использованияИспользование jti позволяет реализовать механизм
одноразового или ограниченно повторяемого токена.
Принцип работы:
При выдаче токена создаётся уникальный jti
jti сохраняется на сервере (например, в
Redis)
При каждом запросе выполняется проверка:
jtiПосле использования jti может помечаться как
“spent”
Таким образом, даже при компрометации JWT повторное использование становится невозможным.
jti с использованием библиотеки joseБиблиотека jose предоставляет инструменты для создания и проверки
JWT, но генерация jti обычно реализуется на уровне
приложения.
Пример создания токена с уникальным идентификатором:
import { SignJWT } from 'jose';
import { randomUUID } from 'crypto';
const secret = new TextEncoder().encode('super-secret-key');
async function createToken(userId) {
const jti = randomUUID();
const jwt = await new SignJWT({ sub: userId })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('15m')
.setJti(jti)
.sign(secret);
return { jwt, jti };
}
Здесь setJti() фиксирует уникальный идентификатор внутри
токена, который затем можно извлекать и проверять при валидации.
jti при
верификации JWTПри обработке входящего токена выполняется не только проверка подписи
и срока действия, но и контроль уникальности jti.
import { jwtVerify } from 'jose';
const usedJtiStore = new Map(); // условный пример, в реальности Redis
async function verifyToken(token) {
const { payload } = await jwtVerify(token, secret);
const jti = payload.jti;
if (!jti) {
throw new Error('Missing jti');
}
if (usedJtiStore.has(jti)) {
throw new Error('Replay detected');
}
usedJtiStore.set(jti, true);
return payload;
}
В реальных системах вместо Map используется Redis с TTL,
чтобы автоматически очищать устаревшие записи.
jti и управление жизненным цикломЭффективная защита от replay-атак требует корректной стратегии хранения:
jti:<value>exp - now)SET jti:abc123 USED EX 900
jti не записывается как
использованныйВ системах с повышенными требованиями безопасности применяется модель одноразовых JWT:
jti блокируетсяТакой подход часто используется в:
exp и jti для защиты от перегрузки
хранилищаПрямая запись всех jti без ограничения времени приводит
к росту хранилища. Поэтому критически важно синхронизировать TTL записи
с временем жизни токена:
exp определяет верхнюю границу валидностиexp - nowЭто обеспечивает автоматическую очистку без дополнительных cron-задач.
jtiРаспространённые проблемы реализации:
jti без криптографической стойкости
(Math.random)jti без проверки повторного
использованияjti между разными сервисами без
namespaceВ микросервисной среде проверка jti требует
централизованного или синхронизированного хранилища:
Каждый сервис:
jtiАтомарная операция предотвращает race condition при параллельных запросах.
Классическая реализация:
import { createClient } from 'redis';
const redis = createClient();
async function checkJti(jti) {
const key = `jti:${jti}`;
const result = await redis.set(key, '1', {
NX: true,
EX: 900
});
if (!result) {
throw new Error('Replay detected');
}
}
Операция SET NX гарантирует, что ключ создаётся только
один раз.
jti с
refresh-token стратегиямиВ архитектурах с refresh-token jti может использоваться
отдельно:
jti, обязательно хранится в
базеjti
инвалидируетсяЭто создаёт цепочку контроля с возможностью отзыва сессии.
Комбинированный подход включает несколько уровней:
expjtiТакая схема делает повторное использование токена либо невозможным, либо легко обнаруживаемым и блокируемым.