Какие плюсы и минусы у JSON Web Tokens (JWT) для аутентификации в Node.js приложениях?

«Какие плюсы и минусы у JSON Web Tokens (JWT) для аутентификации в Node.js приложениях?» — вопрос из категории Безопасность, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Плюсы JWT в Node.js:

  • Stateless (без состояния): Серверу не нужно хранить сессию в памяти или БД. Это упрощает архитектуру и горизонтальное масштабирование: любой инстанс приложения может проверить токен.
  • Самодостаточность: В payload токена можно хранить базовые данные о пользователе (userId, роль), уменьшая количество запросов к БД.
  • Безопасность подписи: Использование алгоритмов вроде HS256 или RS256 гарантирует, что токен не был изменен после выдачи.
  • Универсальность: Легко использовать между различными доменами (CORS) и типами клиентов (веб, мобильные приложения, микросервисы).

Минусы и риски JWT:

  • Немедленный отзыв невозможен: Токен валиден до истечения срока (exp). Для решения используют короткоживущие access-токены (минуты) и долгоживущие refresh-токены, которые можно отозвать на сервере.
  • Размер: Токен больше, чем session ID, и передается с каждым запросом, что увеличивает трафик.
  • Уязвимость к XSS при неправильном хранении: Если хранить JWT в localStorage, он доступен для скриптов. Более безопасный способ — использовать httpOnly куки (хотя это усложняет работу с мобильными приложениями).
  • Данные устаревают: Информация в payload может не соответствовать актуальным данным в БД (например, если роль пользователя изменилась).

Пример реализации с библиотекой jsonwebtoken:

// Генерация токена
const jwt = require('jsonwebtoken');
const accessToken = jwt.sign(
  { userId: 123, role: 'user' }, // payload
  process.env.JWT_SECRET,        // секретный ключ
  { expiresIn: '15m' }           // срок жизни
);

const refreshToken = jwt.sign(
  { userId: 123 },
  process.env.JWT_REFRESH_SECRET,
  { expiresIn: '7d' }
);

// Middleware для проверки токена в Express
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer <token>

  if (!token) return res.sendStatus(401);

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.sendStatus(403); // Невалидный или просроченный токен
    req.user = user; // Добавляем данные пользователя в объект запроса
    next();
  });
}

app.get('/protected', authenticateToken, (req, res) => {
  res.json({ message: `Hello user ${req.user.userId}` });
});