Схема Dual JWT — это вариант реализации OAuth 2.0 (RFC 6749), где аутентификация строится на двух JSON Web Token: access-токен передаётся в заголовке Authorization, а refresh-токен хранится в cookie. Оба токена — stateless-JWT, то есть сервер не хранит их в базе. Для отзыва сессий используется deny list в Redis. Цель схемы — снизить нагрузку на инфраструктуру, сохраняя достаточно высокий, но не максимальный уровень безопасности. В этой статье разберём, как устроена схема, какие у неё компромиссы и как внедрить её без типичных ошибок.

Как работает Dual JWT

Пользователь проходит аутентификацию (логин/пароль, OAuth-провайдер или иной способ). Сервер выдаёт пару токенов: access-JWT с коротким сроком жизни (например, 5–15 минут) и refresh-JWT с более длинным сроком (например, 7–30 дней). Access-токен клиент отправляет в заголовке Authorization: Bearer <token> при каждом запросе к API. Refresh-токен хранится в HttpOnly, Secure, SameSite cookie и используется только для получения новой пары токенов через отдельный эндпоинт.

Оба токена подписаны секретным ключом (или асимметричным алгоритмом) и содержат claims: идентификатор пользователя, срок действия, роль, идентификатор сессии. Сервер проверяет подпись и срок действия без обращения к хранилищу — это и есть stateless-подход. Единственное исключение — deny list: если сессию нужно отозвать досрочно (например, при выходе из системы или компрометации), идентификатор сессии добавляется в Redis с TTL, равным оставшемуся сроку жизни refresh-токена. При проверке access-токена сервер дополнительно смотрит в deny list, но только для тех сессий, которые были отозваны.

Параметры безопасности и компромиссы

Схема не претендует на максимальную защиту, но покрывает типовые угрозы. Короткий срок жизни access-токена снижает ущерб от его утечки. Refresh-токен в HttpOnly cookie защищён от кражи через XSS, но требует защиты от CSRF — для этого используется SameSite=Strict или Lax, а также проверка Origin/Referer на эндпоинте обновления токена. Deny list в Redis позволяет отзывать сессии, но добавляет сетевой вызов при проверке токена. Чтобы минимизировать задержки, deny list можно кэшировать в памяти процесса или использовать локальный кэш с коротким TTL.

Важно понимать: Dual JWT не заменяет многофакторную аутентификацию и не защищает от фишинга. Это базовая схема для сервисов с умеренными требованиями к безопасности. Если у вас финансовое приложение или медицинские данные,可能需要 дополнительные меры: привязка токена к IP или устройству, ротация refresh-токенов, обнаружение аномалий.

Экономика решения: за счёт чего снижаются затраты

Stateless-JWT убирает необходимость хранить сессии в базе данных и делать запрос к ней на каждом запросе. Это снижает нагрузку на БД и уменьшает задержки. Deny list в Redis — лёгкое хранилище с TTL, которое не требует сложных запросов. В результате инфраструктура обходится дешевле: меньше ресурсов на базу, меньше реплик, проще масштабирование. Однако есть и затраты: Redis нужно поддерживать, а при проверке токена — обращаться к нему. Если deny list пуст, можно оптимизировать проверку: сначала смотреть в локальный кэш, затем в Redis только при промахе.

Схема хорошо подходит для микросервисов и API, где важна горизонтальная масштабируемость. Но если у вас монолит с небольшим числом пользователей, выгода может быть неочевидной. Считайте: стоимость Redis, стоимость разработки и поддержки, стоимость возможных инцидентов безопасности. Для стартапов с ограниченным бюджетом Dual JWT — разумный компромисс.

Шаги внедрения и типичные ошибки

Вот последовательность действий, которая поможет внедрить схему без боли:

  • Определите сроки жизни токенов. Access — 5–15 минут, refresh — от 7 до 30 дней. Чем короче access, тем безопаснее, но чаще обновление.
  • Настройте cookie для refresh-токена. Флаги HttpOnly, Secure, SameSite=Strict (или Lax, если нужна кросс-доменная работа). Путь cookie — только эндпоинт обновления.
  • Реализуйте эндпоинт обновления токена. Он принимает refresh-токен из cookie, проверяет его подпись и срок, проверяет deny list, выдаёт новую пару. Старый refresh-токен можно инвалидировать (ротация) или оставить до истечения срока.
  • Добавьте deny list в Redis. При выходе из системы или принудительном отзыве сессии добавляйте идентификатор сессии (jti) в Redis с TTL, равным оставшемуся сроку refresh-токена. При проверке access-токена проверяйте наличие jti в deny list.
  • Обеспечьте защиту от CSRF. Проверяйте заголовок Origin на эндпоинте обновления, используйте SameSite cookie. Для дополнительной защиты можно применять CSRF-токены.
  • Логируйте и мониторьте. Отслеживайте аномалии: частые обновления токенов, запросы с недействительными подписями, попытки использования отозванных сессий.

Типичные ошибки:

  • Хранение refresh-токена в localStorage. Это делает его уязвимым для XSS. Только HttpOnly cookie.
  • Отсутствие проверки deny list. Тогда отзыв сессии не работает, и украденный refresh-токен можно использовать до истечения срока.
  • Слишком долгий срок жизни access-токена. Увеличивает окно атаки при утечке.
  • Игнорирование CSRF. Refresh-токен в cookie автоматически отправляется с запросами, поэтому эндпоинт обновления должен быть защищён.
  • Отсутствие ротации refresh-токена. Если токен украден, злоумышленник может обновлять его бесконечно. Ротация с инвалидацией старого токена помогает обнаружить кражу.

Пример: сервис с 10 000 активных пользователей. При использовании сессий в БД каждый запрос к API требует запроса к базе. С Dual JWT запросы обрабатываются без обращения к БД, а deny list проверяется только для отозванных сессий. Это снижает нагрузку на БД в разы и позволяет обходиться меньшим количеством реплик. Однако нужно добавить Redis и настроить мониторинг. В итоге экономия на инфраструктуре может достигать 20–30% при сопоставимом уровне безопасности.

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