JWT широко используется для аутентификации в REST API и SPA. В этой статье рассмотрена пошаговая реализация: логин, выдача токенов, middleware проверки и хранение на клиенте.
Общая схема
- Пользователь отправляет логин и пароль
- Сервер проверяет учётные данные, генерирует JWT
- Клиент сохраняет токен и отправляет его в заголовке при запросах
- Сервер проверяет подпись и claims, возвращает данные или 401
Выдача токена (Node.js + Express)
const jwt = require('jsonwebtoken');
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = await findUserByEmail(email);
if (!user || !await verifyPassword(password, user.passwordHash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token, expiresIn: 3600 });
});
Проверка токена (middleware)
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid token' });
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload;
next();
} catch (e) {
if (e.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
Защита маршрутов
app.get('/api/profile', authMiddleware, (req, res) => {
res.json({ user: req.user });
});
Хранение на клиенте
Вариант 1: localStorage
// После логина
localStorage.setItem('token', data.token);
// При запросах
const token = localStorage.getItem('token');
fetch('/api/profile', {
headers: { Authorization: `Bearer ${token}` }
});
Минус: уязвимость к XSS. Если скрипт получает доступ к странице, он может прочитать токен.
Вариант 2: httpOnly cookie
Сервер устанавливает cookie при логине:
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
});
Браузер автоматически отправляет cookie с запросами. JavaScript не имеет доступа к cookie — защита от XSS. Требуется настройка CORS и credentials.
Вариант 3: Память + refresh
Access token в памяти (переменная), refresh token в httpOnly cookie. При перезагрузке страницы — запрос нового access token через refresh.
Обновление токена (refresh)
app.post('/api/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
try {
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await findUserById(payload.sub);
if (!user) return res.status(401).json({ error: 'User not found' });
const accessToken = jwt.sign(
{ sub: user.id },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ token: accessToken });
} catch (e) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
Инструменты для отладки
JWT Decoder на rechecker.ru позволяет декодировать токен и просмотреть payload. Используйте только с тестовыми токенами.
Рекомендации по безопасности
- Используйте HTTPS
- Короткое время жизни access token (15–60 минут)
- Refresh token в httpOnly cookie
- Секреты в переменных окружения, не в коде
- Для публичных API — RS256 вместо HS256
Обработка истечения на клиенте
Перехватывайте 401 и перенаправляйте на логин или обновляйте токен:
async function fetchWithAuth(url, options = {}) {
let token = getToken();
let response = await fetch(url, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${token}` }
});
if (response.status === 401) {
const refreshed = await refreshToken();
if (refreshed) {
token = getToken();
response = await fetch(url, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${token}` }
});
}
}
return response;
}
Логаут
При localStorage — удалить токен. При cookie — сервер должен очистить cookie в ответе на запрос логаута. Для refresh token — добавить в blacklist или удалить из хранилища на сервере.
Дополнительные материалы
Структура JWT, алгоритмы и основы безопасности — в JWT токены: как работают, структура и безопасность.
CORS при использовании cookie
При хранении токена в cookie и запросах с другого домена настройте CORS:
app.use(cors({
origin: 'https://frontend.example.com',
credentials: true
}));
Клиент должен отправлять credentials: 'include' в fetch.
Проверка ролей
После верификации токена проверяйте claims:
function requireRole(role) {
return (req, res, next) => {
if (req.user?.role !== role) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.get('/api/admin', authMiddleware, requireRole('admin'), handler);
Rate limiting для логина
Ограничьте количество попыток входа с одного IP для защиты от брутфорса. Используйте Redis или in-memory store для счётчиков.
Логирование
Логируйте успешные и неуспешные попытки аутентификации (без паролей и токенов). Это помогает при расследовании инцидентов.
Тестирование
Для тестов используйте тестовые токены с коротким временем жизни. Мокайте middleware аутентификации в unit-тестах. В E2E-тестах получайте токен через тестовый логин или фикстуру.
Мультитенантность
При нескольких приложениях (issuers) используйте iss и aud для проверки. Храните публичные ключи или секреты по issuer. При верификации проверяйте, что iss соответствует ожидаемому издателю.
Миграция с сессий на JWT
Пошагово: добавьте выдачу JWT при логине параллельно с сессией. Клиенты переключаются на JWT. После полного перехода отключите сессии. Убедитесь, что refresh flow и логаут работают корректно.
Безопасность refresh token
Refresh token должен храниться безопаснее access token: httpOnly cookie, защищённое хранилище. При компрометации — немедленная инвалидация. Рассмотрите привязку к устройству или fingerprint для дополнительной защиты.
Логирование событий аутентификации
Логируйте: успешный логин, неудачный логин (с IP, без пароля), истечение токена, refresh. Это необходимо для аудита и расследования инцидентов. Не логируйте сами токены и пароли.