JWT (JSON Web Tokens)
Зачем нужно
JWT — стандарт токенов для аутентификации и авторизации. Токен содержит информацию о пользователе (claims) в зашифрованном виде. Сервер не хранит сессии — он проверяет подпись токена и извлекает данные. Это делает JWT идеальным для stateless API и микросервисов.
Где используется
- Аутентификация в SPA (React, Vue, Angular)
- API авторизация (REST, GraphQL)
- Микросервисы — передача identity между сервисами
- OAuth 2.0 — access token часто реализуют как JWT
- Single Sign-On (SSO)
Структура JWT
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header.Payload.Signature
Три части, разделённые точками, закодированные в Base64URL
Header (заголовок)
// Base64URL decode первой части
{
"alg": "HS256", // Алгоритм подписи
"typ": "JWT" // Тип токена
}
// Алгоритмы: HS256 (HMAC), RS256 (RSA), ES256 (ECDSA)
Payload (полезная нагрузка)
// Base64URL decode второй части
{
// Registered claims (стандартные)
"iss": "myapp.com", // Издатель (issuer)
"sub": "42", // Субъект (subject) — ID пользователя
"exp": 1700000000, // Время истечения (expiration)
"iat": 1699996400, // Время создания (issued at)
"nbf": 1699996400, // Не раньше (not before)
// Custom claims (пользовательские)
"userId": 42,
"role": "admin",
"email": "anton@mail.ru"
}
Signature (подпись)
// Подпись проверяет целостность токена
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret // Секретный ключ, известный только серверу
)
// Если кто-то изменит payload — подпись не совпадёт
// Сервер отклонит токен
Access Token + Refresh Token
Access Token:
- Короткоживущий (15 мин — 1 час)
- Отправляется с каждым запросом
- Содержит данные пользователя
- При утечке — минимальный ущерб
Refresh Token:
- Долгоживущий (7-30 дней)
- Используется ТОЛЬКО для получения нового access token
- Хранится в httpOnly cookie (или secure storage)
- Можно отозвать на сервере
// Схема работы:
// 1. Логин → получаем access + refresh
// 2. Запросы к API с access token
// 3. Access истёк (401) → отправляем refresh → получаем новый access
// 4. Refresh истёк → перелогин
// Логин
async function login(email, password) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const { accessToken, refreshToken } = await response.json();
// Access token — в памяти (переменная)
setAccessToken(accessToken);
// Refresh token — в httpOnly cookie (сервер ставит через Set-Cookie)
// Или если API отдаёт в body — сохранить в localStorage (менее безопасно)
}
// Refresh
async function refreshAccessToken() {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include', // Отправить httpOnly cookie с refresh token
});
if (!response.ok) {
// Refresh token истёк — перенаправить на логин
window.location.href = '/login';
return;
}
const { accessToken } = await response.json();
setAccessToken(accessToken);
return accessToken;
}
Реализация на сервере (Node.js)
npm install jsonwebtoken bcrypt
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const ACCESS_SECRET = process.env.ACCESS_SECRET;
const REFRESH_SECRET = process.env.REFRESH_SECRET;
// Генерация токенов
function generateTokens(user) {
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// Логин
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findByEmail(email);
if (!user) return res.status(401).json({ error: 'Неверные данные' });
const validPassword = await bcrypt.compare(password, user.passwordHash);
if (!validPassword) return res.status(401).json({ error: 'Неверные данные' });
const { accessToken, refreshToken } = generateTokens(user);
// Refresh token в httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 дней
});
res.json({ accessToken });
});
// Middleware для проверки access token
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Нет токена' });
}
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, ACCESS_SECRET);
req.user = payload; // { userId, role }
next;
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Токен истёк' });
}
return res.status(401).json({ error: 'Невалидный токен' });
}
}
// Refresh
app.post('/api/auth/refresh', (req, res) => {
const { refreshToken } = req.cookies;
if (!refreshToken) return res.status(401).json({ error: 'Нет refresh token' });
try {
const payload = jwt.verify(refreshToken, REFRESH_SECRET);
const user = { id: payload.userId, role: 'user' }; // Из БД в реальности
const tokens = generateTokens(user);
res.cookie('refreshToken', tokens.refreshToken, {
httpOnly: true, secure: true, sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken: tokens.accessToken });
} catch {
res.status(401).json({ error: 'Невалидный refresh token' });
}
});
// Защищённый endpoint
app.get('/api/profile', authMiddleware, (req, res) => {
res.json({ userId: req.user.userId, role: req.user.role });
});
Стратегии хранения токенов
| Хранилище | XSS | CSRF | Удобство |
|---|---|---|---|
| localStorage | Уязвим | Защищён | Просто |
| httpOnly cookie | Защищён | Уязвим | Средне |
| Память (переменная) | Защищён | Защищён | Теряется при reload |
Рекомендация: Access token — в памяти (переменная). Refresh token — в httpOnly cookie.
Частые ошибки
- Секрет в коде —
jwt.sign(data, 'my-secret')— секрет должен быть в.env - Нет
exp— токен без срока действия работает вечно - Хранят в localStorage — XSS-атака крадёт токен мгновенно
- Слишком долгий access token — 30 дней access token = нет смысла в refresh
- Нет отзыва refresh token — при компрометации нельзя разлогинить пользователя
- Чувствительные данные в payload — JWT только кодируется (Base64), не шифруется
Практика
- Реализовать генерацию и верификацию JWT (jsonwebtoken)
- Создать login endpoint, возвращающий access + refresh tokens
- Реализовать authMiddleware для защиты endpoints
- Реализовать refresh flow с httpOnly cookie
- Написать клиентский interceptor для автоматического refresh при 401
Связанные темы
- OAuth 2.0 — JWT как access token в OAuth
- Cookies и сессии — альтернативный подход
- HTTP протокол — Authorization header
- CORS — credentials при кросс-доменных запросах