Ключевым элементом современных веб-архитектур стало использование JSON Web Tokens (JWT) для управления доступом. Часто приходится видеть, как система, построенная по "5-минутному гайду", открывает серьёзные уязвимости. Рассмотрим недооцененные аспекты их применения — от ротации секретов до борьбы с чёрными списками.
От генерации до промежуточного ПО: шаблоны ошибок
Начнём с классического экспресс-мидлвара:
// Типичная опасная реализация
app.post('/login', (req, res) => {
const user = authenticate(req.body);
const token = jwt.sign({ sub: user.id }, 'your-256-bit-secret', { expiresIn: '30d' });
res.json({ token });
});
// Проверка в мидлваре
const authenticateJWT = (req, res, next) => {
const token = req.header('Authorization')?.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, 'your-256-bit-secret', (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
};
Две критические проблемы:
- Жёстко закодированный секрет
- Отсутствие проверки отозванных токенов
Динамические истории вращения ключей
Статичные секреты — гарантированная угроза при компрометации. Решение:
// Генерация ротируемого секрета
const keyPairs = [{
keyId: '2024-06-k1',
publicKey: readFileSync('public_key.pem'),
privateKey: readFileSync('private_key.pem')
}];
app.post('/login', (req, res) => {
const user = authenticate(req.body);
const currentKey = keyPairs[0]; // Используем активный ключ
const token = jwt.sign(
{ sub: user.id },
currentKey.privateKey,
{
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'api.example.com',
keyid: currentKey.keyId
}
);
res.json({ token: token, keyId: currentKey.keyId });
});
// Проверяющий мидлвар
const verifyToken = (token) => {
const header = jwt.decode(token, { complete: true })?.header;
if (!header?.kid) throw new Error('Invalid token');
const keyPair = keyPairs.find(k => k.keyId === header.kid);
if (!keyPair) throw new Error('Key not found');
return jwt.verify(token, keyPair.publicKey, {
algorithms: ['RS256'],
issuer: 'api.example.com'
});
};
Ротация ключей ежемесячно позволяет:
- Автоматизировать вывод старых ключей из эксплуатации
- Настроить разный уровень защиты токенов в базе данных
- Идентифицировать источник угроз при компрометации одного из ключей
Динамическое аннулирование токенов через blacklist
В отличие от сессий, stateless-токены сложнее отзывать. Паттерн с "чёрными списками" требует ювелирной реализации:
// Redis хранилище для отозванных токенов
const revokedTokens = createRedisClient();
// Регистрация отозванного токена с TTL экспирации
const revokeToken = async (token) => {
const { jwtid, exp } = jwt.decode(token);
await revokedTokens.set(`revoked:${jwtid}`, 'true', 'PX', exp * 1000 - Date.now());
};
// Обновлённый верификатор
const verifyToken = async (token) => {
const decoded = verifyToken(token);
// Проверка на аннулирование
const keyId = decoded.jti;
const isRevoked = await revokedTokens.get(`revoked:${keyId}`);
if (isRevoked) throw new Error('Token revoked');
return decoded;
};
// Отмена токена при смене пароля пользователя
app.post('/reset-password', authenticateJWT, async (req, res) => {
await revokeToken(req.headers.authorization.split(' ')[1]);
// ...смена пароля
res.sendStatus(204);
});
Мониторинг размера хранилища критичен — учитывайте период жизни токена при установке TTL.
Тактика для короткой жизни токенов
Сокращайте время жизни access-токенов до минут, применяя refresh-механизм:
// Параметры токенов
const ACCESS_TOKEN_EXP = '15m';
const REFRESH_TOKEN_EXP = '7d';
// Логин возвращает пару токенов
app.post('/login', (req, res) => {
const user = authenticate(req.body);
const accessToken = generateAccessToken(user.id);
const refreshToken = jwt.sign({ jti: generateId() }, refreshSecret, {
subject: user.id,
expiresIn: REFRESH_TOKEN_EXP
});
// Безопасная передача refreshToken в httpOnly cookie
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'Strict',
maxAge: 604800000 // 7 дней
}).json({ accessToken });
});
// Обновление пары токенов
app.post('/refresh', async (req, res) => {
const refreshToken = req.cookies.refresh_token;
try {
const payload = jwt.verify(refreshToken, refreshSecret);
// Отзываем использованный refresh токен
await revokeToken(refreshToken);
const newAccessToken = generateAccessToken(payload.sub);
const newRefreshToken = jwt.sign({ jti: generateId() }, refreshSecret, {
subject: payload.sub,
expiresIn: REFRESH_TOKEN_EXP
});
res.cookie('refresh_token', newRefreshToken, secureCookieOptions)
.json({ accessToken: newAccessToken });
} catch (err) {
res.clearCookie('refresh_token').sendStatus(401);
}
});
Развертывание экосистемы
Реализовать такие практики — только начало. Мониторинг должен включать:
- Графики частоты отзывов токенов
- Сигналы на внезапные всплески аутентификационных запросов
- Алгоритм автоматической ротации ключей при росте ошибок верификации
- 1 ключ для высококритичных систем, 4 ключа для общедоступного API — запаса на случай компрометации
Микросервисная архитектура требует централизованного кейманагера и логауншеров:
sequenceDiagram
participant C as Client
participant A as Auth Service
participant B as Business Service
participant R as Revoked Tokens Store
C->>A: Login
A->>C: AccessToken + HttpOnly Cookie
C->>B: Request with AccessToken
B->>A: Introspect token
A->>R: Проверка отзыва
R->>A: Статус токена
A->>B: User data
B->>C: Response
При проектировании узлов интроспекции токенов ключевое внимание уделяйте:
- Географическому расположению относительно сервисов
- Обеспечению стабильного времени ответа
- Поддержанию нескольких мест хранения чёрных списков
Результат продуманной стратегии
Применяя динамическую криптографию и механизмы аннулирования, достигаем баланса между производительностью API и контролем безопасности. Не забывайте, что даже лучшие реализации требуют:
- Автоматического перевыпуска ключей каждые 30-90 дней
- "Затенения" активности пользователей при обновлении токенов
- Идентификаторов токенов в трендах мониторинга
Хорошая новость: современные платформы (Kong, Apollo GraphQL) вторично предоставляют инструменты для управления устройствами проверки. Код примеров иллюстрирует концепции, а не слепое копирование — адаптируйте тактику под данные угроз. Не на миллион пользователей? Логируйте отзывы в оперативную память приложения. Особенность JWT не в их простоте использования — в возможности решать комплексные задачи аутентификации без фатальных компромиссов.