Що таке JWT і як він працює?
JWT (JSON Web Token) - це компактний самодостатній токен, який кодує дані про користувача в JSON, підписує їх криптографічно і дозволяє серверу перевіряти особу без запиту до бази даних.
Теорія
TL;DR
- JWT схожий на запечатаний паспорт: дані видно всім, але будь-яка зміна ламає криптографічний підпис
- Автентифікація без збереження стану: немає запиту до БД на кожен запит, на відміну від сесій
- Три частини у Base64Url, з'єднані крапками:
header.payload.signature - Payload читається будь-ким. JWT підписаний, але не зашифрований
- JWT - для API і мікросервісів; сесії - коли потрібне миттєве скасування доступу
Швидкий приклад
const jwt = require('jsonwebtoken');
// Створення токена при логіні
const token = jwt.sign(
{ userId: 123, role: 'admin' },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
// eyJhbGciOiJIUzI1NiJ9...
// Перевірка на кожному захищеному запиті
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) return res.status(401).json({ error: 'Unauthorized' });
// decoded = { userId: 123, role: 'admin', iat: ..., exp: ... }
});sign створює токен. verify перераховує підпис і порівнює байт за байтом із третім сегментом. Зміни хоч щось у payload - підписи не збіжаться, і запит провалиться.
Структура JWT
JWT виглядає так: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyM30.SflKxwRJSMeKKF2QT4fw
Три частини, дві крапки. Кожна частина кодується у Base64Url - це URL-безпечний варіант Base64 без символів + і =, щоб токен не ламався всередині HTTP-заголовків.
Header вказує алгоритм підпису:
{ "alg": "HS256", "typ": "JWT" }Payload несе claims (твердження про користувача):
{ "sub": "123", "role": "admin", "exp": 1716239022, "iat": 1716235422 }Стандартні claims: sub (ID користувача), iss (видавець), aud (отримувач), exp (час закінчення дії), iat (час створення), nbf (не дійсний до). Поруч можна додавати власні поля.
Signature - рівень безпеки:
HMACSHA256(base64Url(header) + "." + base64Url(payload), secret)
Сервер перераховує цей підпис на кожному запиті. Збіг - токен валідний і незмінений. Розбіжність - хтось підробив дані.
Як працює підпис
Коли ти викликаєш jwt.sign(), Node запускає HMAC-SHA256 над закодованими header і payload, використовуючи твій секретний ключ. Результат і стає третім сегментом рядка токена.
При jwt.verify() те саме обчислення виконується над отриманим header.payload. Бібліотека порівнює результат із третім сегментом через constant-time comparison, що захищає від timing-атак. Якщо exp менший за поточний Unix-час, перевірка провалюється незалежно від підпису.
Саме тому payload будь-якого JWT можна вставити на jwt.io і прочитати всі поля. Підпис не ховає дані. Він лише доводить, що дані не змінювались після видачі токена.
Головна різниця від сесій
З сесіями сервер зберігає стан: ID користувача, ролі та інші дані живуть у пам'яті або базі даних, і кожен запит викликає пошук. З JWT сервер нічого не зберігає. Токен несе всі дані, а перевірка - це чисто криптографічна операція.
Це добре масштабується горизонтально, бо будь-який екземпляр сервера може перевірити будь-який токен зі спільним секретом. Але є компроміс: токен залишається валідним до exp, навіть після логауту. Сесії дозволяють видалити стан негайно. Я бачив, як команди будували чорний список токенів у пам'яті процесу для логауту, а потім втрачали весь список при кожному деплої. Короткий строк дії разом із серверними refresh токенами - єдине рішення, яке тримається в продакшені.
Коли використовувати
- API-only або мікросервіси: JWT (горизонтальне масштабування)
- Один сервер зі сховищем сесій: cookies і сесії (простіше скасування)
- Мобільні додатки або SPA: JWT (зручно додавати до HTTP-заголовків)
- OAuth2 і OIDC потоки: JWT (стандартні claims, Auth0, Okta, AWS Cognito)
- Довгі сесії зі скасуванням: короткі access токени плюс серверні refresh токени
Типові помилки
Чутливі дані в payload. Payload - це просто Base64. Будь-хто може вставити токен на jwt.io і прочитати кожне поле. Зберігай тільки opaque ID і ролі. Чутливі дані отримуй із БД після перевірки токена.
Неправильно:
jwt.sign({ sub: 'user', creditCard: '4111...' }, secret);Правильно:
jwt.sign({ sub: 'user123', role: 'admin' }, secret, { expiresIn: '15m' });Токен без expiration або зі слабким секретом.
// Неправильно: токен живе вічно, секрет легко зламати
jwt.sign(payload, 'secret');
// Правильно: короткий строк дії, 256-бітний випадковий секрет
jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' });
// Генерація: crypto.randomBytes(32).toString('hex')Без явного обмеження алгоритму. Специфікація JWT дозволяє alg: none, що прибирає підпис повністю. Зловмисник змінює payload і виставляє алгоритм none.
// Неправильно: приймає будь-який алгоритм, включаючи "none"
jwt.verify(token, secret);
// Правильно: явний whitelist
jwt.verify(token, secret, { algorithms: ['HS256'] });JWT в localStorage. Будь-який XSS-скрипт може прочитати localStorage і вкрасти токен. Використовуй httpOnly cookie або тримай токени короткоживучими з refresh-флоу.
JWT без реальної стратегії відкликання. Токен звільненого співробітника залишається валідним до exp. Рішення: access токени на 15 хвилин плюс refresh токени в Redis, що видаляються при логауті або блокуванні користувача.
Де зустрічається в реальних проектах
- Express +
passport-jwt: витягує і перевіряєAuthorization: Bearer <token>у middleware - React/Next.js:
fetchабоaxiosдодає токен до заголовка кожного API-запиту - Auth0, Okta, AWS Cognito: видають JWT після логіну, верифікація через JWKS endpoint
- GraphQL + Apollo: функція
contextвитягує і декодує JWT для resolver'ів - HS256 vs RS256: HS256 - симетричний (один секрет для підпису і перевірки, швидко); RS256 - асиметричний (приватний ключ підписує, публічний перевіряє, краще для мультисервісних архітектур)
Питання на співбесіді
Q: Опиши шлях JWT від логіну до API-виклику.
A: Користувач відправляє дані входу, сервер підписує payload і повертає токен. Клієнт додає його як Authorization: Bearer <token> на кожному наступному запиті. Middleware перевіряє підпис і передає claims в обробник для авторизаційної логіки.
Q: Як відкликати JWT?
A: Короткий строк дії access токена (15 хвилин) плюс серверні refresh токени в Redis. При логауті видали refresh токен. Для додаткового захисту: claim jti і чорний список для критичних операцій.
Q: Різниця між HS256 і RS256?
A: HS256 - симетричний: один і той самий секрет для підпису і перевірки. RS256 - асиметричний: приватний ключ підписує, будь-який сервіс із публічним ключем перевіряє. RS256 краще підходить коли кілька сервісів перевіряють токени і ділитись секретом між ними небезпечно.
Q: Навіщо Base64Url замість звичайного Base64?
A: Звичайний Base64 використовує +, / і =, які ламають HTTP-заголовки і URL. Base64Url замінює їх на безпечні аналоги, тому токен передається без спотворень.
Q (Senior): Спроектуй систему JWT-авторизації без downtime для мільйона користувачів.
A: Короткі access токени (5 хвилин) плюс 24-годинні refresh токени в Redis-кластері з TTL рівним часу дії refresh токена. Ротація при кожному використанні: старий видаляється, нова пара видається. Для ротації ключів - JWKS endpoint із коротким TTL кешу на сервісах-клієнтах. Моніторинг latency верифікації підписів у міру зростання Redis.
Приклади
Логін і захищений роут
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.post('/login', express.json(), (req, res) => {
// Замінити на реальну перевірку з БД
if (req.body.email !== 'user@example.com' || req.body.password !== 'pass') {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign(
{ id: 1, email: req.body.email },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ token });
});
app.get('/profile', (req, res) => {
const token = req.headers.authorization?.split(' ')[1]; // Bearer <token>
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256']
});
res.json({ user: decoded });
} catch (err) {
res.status(401).json({ error: 'Unauthorized' });
}
});/login повертає { token: "eyJ..." }. Клієнт зберігає токен і відправляє Authorization: Bearer eyJ... на кожному запиті. /profile перевіряє підпис і повертає декодований об'єкт користувача. Явна опція algorithms - це захист від атаки через alg: none.
Ротація refresh токенів (refresh token rotation)
Коли access токен закінчується, клієнту потрібен спосіб отримати новий без повторного логіну. Ротація refresh токенів вирішує це і одночасно обмежує шкоду від крадіжки токена.
const refreshTokens = new Map(); // В продакшені - Redis
app.post('/refresh', express.json(), (req, res) => {
const { refreshToken } = req.body;
if (!refreshTokens.has(refreshToken)) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
try {
const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
// Ротація: старий токен видалено, видаємо нову пару
refreshTokens.delete(refreshToken);
const newAccess = jwt.sign(
{ id: decoded.id },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const newRefresh = jwt.sign(
{ id: decoded.id },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);
refreshTokens.set(newRefresh, true);
res.json({ accessToken: newAccess, refreshToken: newRefresh });
} catch {
res.status(403).json({ error: 'Refresh failed' });
}
});Якщо зловмисник вкраде refresh токен, він зможе використати його один раз. Але при наступній легітимній ротації вкрадений токен видаляється зі сховища і провалюється при наступній спробі. Цей патерн описаний у рекомендаціях Auth0 і Okta.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.