JWT: или как не завалить аутентификацию в продакшене

Введение: почему JWT — не волшебная таблетка

JWT (JSON Web Token) — это стандарт RFC 7519 для передачи утверждений в виде компактного, самодостаточного токена. На практике это значит, что вместо хранения сессий на сервере или в базе вы можете передавать пользовательские данные в зашифрованном виде между клиентом и сервисами. Звучит просто, но реализация часто превращается в кошмар из-за неверных предположений о безопасности, производительности и масштабируемости.

Проблема: Большинство команд начинают использовать JWT, когда уже поздно. Они внедряют его в проекты с уже существующей аутентификацией на сессиях или OAuth, не понимая, что переход на токены требует пересмотра архитектуры безопасности, логирования и даже мониторинга.

Реальный пример: Одна из наших команд перешла с сессий на JWT в монолитном приложении. Через месяц выяснилось, что:

  1. Токены лежат в localStorage и доступны для XSS-атак.
  2. Нет механизма ревокации токенов без перезапуска сервиса.
  3. Логи аутентификации разрознены по микросервисам.

Результат: два неделя на откат и три — на правильную реализацию.


Практика: когда JWT — правильное решение

JWT оправдан в следующих сценариях:

  1. Stateless-архитектура: Когда вам нужно работать с микросервисами или API-гейтами, где сессии на сервере — лишняя головная боль.
  2. Мобильные приложения: Где куки не работают, а токены можно хранить в secure storage.
  3. Высоконагруженные системы: Где сессии на Redis/Memcached требуют дополнительных ресурсов.
  4. Межсервисная аутентификация: Когда сервисы должны доверять друг другу без общих баз данных.

Когда НЕ использовать JWT:

  • В монолитных приложениях с простой аутентификацией (сессии + балансировщик могут быть дешевле).
  • Если у вас нет команды, которая понимает криптографию и безопасность (JWT — это не "просто строка в заголовке").
  • В системах, где требуется частый ревок токенов (например, банковские приложения).

Как правильно внедрять JWT: пошаговая инструкция

1. Выбор алгоритма подписи

Не используйте HS256 в продакшене без веских причин. Почему? Потому что секретный ключ должен быть одинаковым на всех сервисах, и если он утечет — вся система компрометирована.

Лучший выбор: RS256 (RSA с SHA-256). Здесь у вас есть пара ключей — публичный (для валидации) и приватный (для подписи). Публичный ключ можно раздавать всем сервисам, а приватный хранить в секретном менеджере (например, HashiCorp Vault).

Пример конфигурации для Node.js (Express + jsonwebtoken):

const jwt = require('jsonwebtoken');
const fs = require('fs');

// Загрузка ключей из файлов (не из переменных окружения!)
const privateKey = fs.readFileSync('./keys/private.pem', 'utf8');
const publicKey = fs.readFileSync('./keys/public.pem', 'utf8');

// Генерация токена
const token = jwt.sign(
  { userId: 123, role: 'admin' },
  privateKey,
  { algorithm: 'RS256', expiresIn: '1h' }
);

// Валидация токена
jwt.verify(token, publicKey, { algorithms: ['RS256'] }, (err, decoded) => {
  if (err) throw err;
  console.log('Decoded:', decoded);
});

Ключевой момент: Никогда не храните приватный ключ в репозитории или в переменных окружения без шифрования. Используйте инструменты типа sops или Vault для управления секретами.


2. Хранение токенов: где и как

Антипаттерн: Хранение JWT в localStorage или sessionStorage. Почему? Потому что при XSS-атаке токен украдут вместе с данными пользователя.

Правильные варианты:

  1. HTTP-only cookies: Наиболее безопасный способ для веб-приложений. Токен не доступен для JavaScript, но передается автоматически с каждым запросом.

    Set-Cookie: jwt=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; SameSite=Strict
    

    Конфигурация для Express:

    res.cookie('jwt', token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 3600000 // 1 час
    });
    
  2. Secure storage (мобильные приложения): Для iOS/Android используйте Keychain или AndroidKeystore.

  3. Short-lived tokens + refresh tokens: Основной токен живет 15-30 минут, а рефреш-токен — дольше (например, 7 дней). Рефреш-токен должен храниться в HTTP-only cookie или защищенном хранилище.


3. Валидация и middleware

Никогда не доверяйте токену "на слово". Всегда валидируйте его на сервере.

Пример middleware для Express:

const jwt = require('jsonwebtoken');

function authenticateToken(req, res, next) {
  const token = req.cookies.jwt || req.headers.authorization?.split(' ')[1];

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

  jwt.verify(token, publicKey, { algorithms: ['RS256'] }, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

// Использование
app.get('/protected', authenticateToken, (req, res) => {
  res.json({ message: 'Secret data', user: req.user });
});

Что проверять:

  • Алгоритм подписи (убедитесь, что атакующий не может подменить его на none).
  • Поле exp (истечение срока действия).
  • Поле iss (issuer) — чтобы избежать подмены токена от другого сервиса.
  • Поле aud (audience) — если у вас несколько клиентов (например, мобильное приложение и веб-версия).

4. Ревокация токенов

JWT по умолчанию не поддерживает ревокацию без истечения срока действия. Это большая проблема для систем, где пользователь должен быть немедленно выкинут из системы (например, при смене пароля).

Решения:

  1. Short-lived tokens: Сократите время жизни токена до 15-30 минут. Это уменьшает риски, но требует реализации refresh-токенов.
  2. Blacklist: Храните ревокнутые токены в базе данных или Redis. Не забывайте очищать список по истечении времени жизни токена.
    // Пример blacklist middleware
    const redis = require('redis');
    const client = redis.createClient();
    
    async function checkBlacklist(req, res, next) {
      const token = req.cookies.jwt;
      const isBlacklisted = await client.get(token);
      if (isBlacklisted) return res.sendStatus(403);
      next();
    }
    
  3. JWT с поддержкой revocation: Используйте библиотеки типа jwt-decode + ваш сервис ревокации, но это сложно и дорого.

Типичные ошибки и как их избежать

  1. Игнорирование алгоритма подписи

    • Ошибка: Использование HS256 с секретным ключом, раздаваемым всем сервисам.
    • Решение: Всегда используйте RS256 или ES256 (ECDSA).
  2. Хранение токенов в localStorage/sessionStorage

    • Ошибка: Токен доступен для XSS-атак.
    • Решение: Используйте HTTP-only cookies или secure storage.
  3. Отсутствие валидации токена на сервере

    • Ошибка: Доверяете клиенту данные из токена без проверки.
    • Решение: Всегда валидируйте токен на сервере с проверкой всех полей (exp, iss, aud).
  4. Длинные токены в URL

    • Ошибка: JWT может быть до 4 КБ, но некоторые прокси и балансировщики не любят длинные строки.
    • Решение: Передавайте токен в заголовке Authorization: Bearer <token> или в cookie.
  5. Отсутствие механизма ревокации

    • Ошибка: Пользователь не может быть выкинут из системы при смене пароля.
    • Решение: Используйте short-lived tokens + refresh tokens или blacklist.
  6. Неправильная обработка ошибок

    • Ошибка: Возвращаете подробные ошибки (например, "Token expired"), которые могут помочь атакующим.
    • Решение: Всегда возвращайте 401 Unauthorized или 403 Forbidden без деталей.

Производительность и масштабируемость

JWT — это не панацея от всех проблем. Вот что нужно учитывать:

  1. Размер токена: JWT — это Base64-закодированный JSON. Даже с минимальными данными токен может быть 1-2 КБ. Это может быть проблемой для мобильных приложений или систем с ограниченной шириной канала.

    • Решение: Минимизируйте данные в токене. Не храните там, что можно получить из базы или кеша.
  2. CPU-нагрузка: Подпись и валидация RSA/ECDSA токенов требует вычислительных ресурсов. В высоконагруженных системах это может стать бутылочным горлышком.

    • Решение: Кешируйте публичные ключи в Redis или используйте hardware security modules (HSM) для ускорения криптографических операций.
  3. Масштабирование: Если у вас много сервисов, каждый должен иметь доступ к публичному ключу. Это может усложнить управление.

    • Решение: Используйте централизованный сервис для выдачи и валидации токенов (например, OAuth 2.0 provider).

Практический вывод: как не завалить продакшен

  1. Начинайте с правильного алгоритма: RS256 или ES256 — это минимум для продакшена. Никаких HS256 без крайней необходимости.

  2. Не храните токены в localStorage: Используйте HTTP-only cookies или secure storage. Если это невозможно — хотя бы шифруйте токен перед хранением.

  3. Сократите время жизни токена: 15-30 минут — оптимальный баланс между удобством и безопасностью. Реализуйте механизм refresh-токенов.

  4. Валидируйте всё: Проверяйте не только подпись, но и все поля токена (exp, iss, aud). Никогда не доверяйте клиенту.

  5. Подумайте о ревокации: Если у вас есть требования к немедленному выкидыванию пользователей — реализуйте blacklist или short-lived tokens.

  6. Логируйте и мониторьте: Отслеживайте количество неудачных попыток валидации токенов, время их жизни и количество ревок. Это поможет выявить атаки на ранней стадии.

  7. Тестируйте под нагрузкой: JWT может стать бутылочным горлышком при высокой нагрузке. Проверяйте производительность валидации токенов в условиях пиковых нагрузок.


Заключение: JWT — инструмент, а не религия

JWT — это мощный инструмент, но он требует ответственного подхода. Не используйте его просто потому, что "все так делают". Анализируйте свои требования к безопасности, производительности и масштабируемости, и только потом принимайте решение.

Помните: JWT не заменяет хорошую архитектуру безопасности. Он должен быть частью системы, а не её основой. Если у вас нет команды, которая понимает криптографию и безопасность — лучше остаться на проверенных сессиях или OAuth.

Итоговый чек-лист перед релизом:

  • Токены подписаны RS256/ES256, а не HS256.
  • Токены хранятся в HTTP-only cookies или secure storage.
  • Время жизни токена не более 30 минут.
  • Есть механизм ревокации (blacklist или short-lived tokens).
  • Все токены валидируются на сервере с проверкой всех полей.
  • Логи аутентификации централизованы и анализируются.

Если вы выполнили все эти пункты — ваш JWT-интеграция готова к продакшену. Если нет — вернитесь и исправьте ошибки. Потому что в безопасности нет места на "почти".