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

Зачем вообще нужен JWT?

При логине пользователя API должен понять кто к нему обращается. В идеале не дёргать для этого БД при каждом запросе. JSON Web Token (JWT) элегантно решает эту задачу.

JWT — это компактный, подписанный пакет данных, содержащий всю нужную информацию для идентификации пользователя. Любой сервис может проверить его локально, без запроса к центральному хранилищу данных.

Но у такого подхода есть и минусы. Давайте посмотрим на все стороны и когда JWT лучше не применять.


От сессий к токенам: как всё менялось

Как было раньше

До JWT большинство веб-приложений использовали серверные сессии. Схема простая:

Пользователь          Сервер              БД / Кэш
    |                     |                    |
    |-- POST /login ----> |                    |
    |                     |-- save session --> |
    |                     |<-- session_id ---- |
    |<-- session_id ----- |                    |
    |                     |                    |
    |-- GET /profile ---> |                    |
    | (с session_id)      |-- lookup id -----> |
    |                     |<-- user data ----- |
    |<-- 200 OK ----------|                    |

Это было удобно, пока сервер был один. Но при росте системы, стали возникать трудности:

  • Общее хранилище — если серверов несколько, нужен общий доступ к хранилищу сессий (Redis, Memcached). Архитектура становится сложнее.

  • Связанность сервисов — каждый микросервис должен обращаться к центральному хранилищу, чтобы проверить пользователя.

  • Дополнительная задержка — каждый запрос требует обращения к хранилищу и ответа.

JWT как ответ на эти проблемы

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

Анатомия JWT: что внутри

JWT состоит из трёх частей, разделённых точками:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9  ← HEADER (заголовок)
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsZXhleSIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcwOTI1MTIwMH0  ← PAYLOAD (данные)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  ← SIGNATURE (подпись)

1. Header (Заголовок)

{
  "alg": "HS256",
  "typ": "JWT"
}

Указывает тип токена и алгоритм подписи. Наиболее распространённые:

  • HS256 — HMAC + SHA-256 (симметричный, один секрет)

  • RS256 — RSA + SHA-256 (асимметричный, пара ключей)

  • ES256 — ECDSA + SHA-256 (асимметричный, компактнее RSA)

2. Payload (Данные, или Claims)

{
  "sub": "1234567890",
  "name": "Алексей Иванов",
  "role": "admin",
  "iat": 1709164800,
  "exp": 1709251200
}

Содержит claims — утверждения о пользователе. Различают:

Тип

Примеры

Описание

Зарегистрированные

sub, exp, iss, aud, iat

Стандартные поля по RFC 7519

Публичные

name, email

Произвольные, но в IANA-реестре

Приватные

role, permissions

Кастомные поля для вашего приложения

⚠️ Важно: payload не зашифрован, только закодирован в Base64. Любой, кто получит токен, может прочитать его содержимое. Никогда не кладите в JWT пароли, номера карт и другие чувствительные данные.

3. Signature (Подпись)

Подпись создаётся так:

HMAC-SHA256(
  base64url(header) + "." + base64url(payload),
  secret
)

Это криптографическая гарантия того, что токен не был изменён. Если кто-то попытается поменять role с user на admin, подпись не совпадёт и токен будет отклонён.

Преимущества JWT

Stateless по своей природе. В токене уже есть вся нужная информация о пользователе. Горизонтальное масштабирование становится тривиальным: просто добавить новый инстанс сервиса.

Быстрая верификация. Криптографическая подпись проверяется очень быстро. А запрос к Redis или PostgreSQL занимает больше времени + сетевая задержка.

Идеален для SSO. JWT лежит в основе OpenID Connect. Один сервис проверяет личность и выдаёт токен, которому доверяют все остальные сервисы. Пользователь входит в систему один раз – и получает доступ ко всему.

Кросс-доменность. В отличие от cookie, JWT в HTTP-заголовке прекрасно работает с любымы доменами, мобильными приложениями, IoT-устройствами и другими программами.

Открытый стандарт. RFC 7519 – есть библиотеки для любого языка программирования. Всё необходимое уже разработано и проверено временем.

Проблемы и ограничения

Поговорим прямо о подводных камнях, о которых не пишут в туториалах.

Нельзя отозвать токен мгновенно

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

Сценарий: админ заблокировал пользователя, но у него уже есть токен, который действует 24 часа. Ещё целые сутки заблокированный пользователь сможет продолжать пользоваться системой.

Решения (все требуют компромиссов):

Подход 1: Короткое TTL (5-15 минут)
  Плюсы: Быстрая инвалидация "почти бесплатно"
  Минусы: Нужен механизм refresh токенов

Подход 2: Blacklist токенов (в Redis)
  Плюсы: Мгновенный отзыв
  Минусы: Добавляется stateful-компонент, нивелируя часть преимуществ JWT

Подход 3: Версионирование токенов
  Плюсы: Гибкость
  Минусы: Нужен запрос к БД при каждой проверке

Как работают refresh-токены (подход 1 в деталях)

Это самая распространённая схема в проде. Суть простая: access-токен живёт 5–15 минут, refresh-токен дни или недели. Когда access истекает, клиент тихо получает новый, не заставляя пользователя логиниться заново.

Схема взаимодействия:

Клиент              Auth-сервис            Ресурс-сервис
  |                     |                       |
  |-- POST /login ----> |                       |
  |<-- access (15m) --- |                       |
  |    refresh (7d)     |                       |
  |                     |                       |
  |-- GET /data (access_token) -------------->  |
  |<-- 200 OK --------------------------------- |
  |                     |                       |
  |  ... 15 минут ...   |                       |
  |                     |                       |
  |-- GET /data (expired) ------------------>   |
  |<-- 401 Unauthorized ----------------------- |
  |                     |                       |
  |-- POST /refresh --> |                       |
  |   (refresh_token)   |                       |
  |<-- new access ----- |                       |
  |                     |                       |
  |-- GET /data (new_access) ---------------->  |
  |<-- 200 OK --------------------------------- |

Реализация на Spring Boot

Выдача токенов при логине:

@PostMapping("/login")
public TokenResponse login(@RequestBody LoginRequest req) {
    User user = userService.authenticate(req.email(), req.password());

    String accessToken = jwtService.generateAccessToken(user);   // TTL: 15 min
    String refreshToken = jwtService.generateRefreshToken(user); // TTL: 7 days

    // Refresh храним в БД, чтобы можно было отозвать
    refreshTokenRepository.save(new RefreshToken(refreshToken, user.getId()));

    return new TokenResponse(accessToken, refreshToken);
}

Обмен refresh-токена на новый access:

@PostMapping("/refresh")
public TokenResponse refresh(@RequestBody RefreshRequest req) {
    // 1. Проверяем подпись и срок жизни
    jwtService.validateRefreshToken(req.refreshToken());

    // 2. Проверяем, что он не отозван (есть в БД)
    RefreshToken stored = refreshTokenRepository
        .findByToken(req.refreshToken())
        .orElseThrow(() -> new InvalidTokenException("Token revoked"));

    // 3. Rotation: старый удаляем, выдаём новую пару
    refreshTokenRepository.delete(stored);
    User user = userService.findById(stored.getUserId());

    String newAccess  = jwtService.generateAccessToken(user);
    String newRefresh = jwtService.generateRefreshToken(user);
    refreshTokenRepository.save(new RefreshToken(newRefresh, user.getId()));

    return new TokenResponse(newAccess, newRefresh);
}

Зачем нужна ротация refresh-токенов

При каждом обновлении выдаётся новый refresh-токен, а старый становится недействительным. Если кто-то попытается воспользоваться украденным токеном после его смены, система выдаст ошибку, так как в БД его уже не будет. Это помогает обнаружить возможную кражу данных.

Устаревшие данные в payload

Допустим, у пользователя изменили роль с admin на user. Его текущий токен ещё хранит "role": "admin". Это не баг, а свойство stateless-архитектуры. При чувствительных операциях это создаёт реальные риски.

Payload виден всем

# Декодируем любой JWT без ключа:
echo "eyJzdWIiOiIxMjM0NTY3ODkwIn0" | base64 -d
# {"sub":"1234567890"}

Подпись гарантирует целостность, но не конфиденциальность. Если нужно скрыть данные, используй JWE (JSON Web Encryption).

Объём токена

ID сессии в cookie обычно занимает 32-64 символа. А JWT с несколькими параметрами может весить уже 300-500 байт. Для API с высокой нагрузкой это может привести к заметному увеличению трафика.

Типичные ошибки реализации

Ошибка

Последствие

Алгоритм "alg": "none"

Подпись не проверяется, токен принимается

Не валидируется exp

"Вечные" токены, которые нельзя отозвать

Не проверяется iss и aud

Токены от чужого сервиса будут приняты

Секрет хранится в коде

Компрометация всей системы при утечке репозитория

Хранение в localStorage

Уязвим к XSS-атакам


Когда стоит выбрать JWT

  • Микросервисная архитектура. Каждый сервис верифицирует токен локально, без единой точки отказа.

  • Мобильные приложения и IoT. JWT в заголовках не зависит от cookie и работает на любой платформе.

  • Single Sign-On. Один IdP выпускает токен, которому доверяют сразу несколько сервисов.

  • Service-to-service auth. Backend-сервисы аутентифицируют друг друга без user context

  • Высоконагруженные API. На 1000+ RPS каждый сохранённый вызов к базе данных имеет значение.

Когда стоит дважды подумать перед использованием JWT

Нужен мгновенный logout. Банки, медицинские системы, корпоративные инструменты — там, где нельзя ждать истечения TTL. Серверные сессии дают мгновенный контроль.

Роли меняются часто. Если права пользователей обновляются по несколько раз в день, токен с TTL 1 час — это 1 час потенциальной дыры в безопасности.

Монолитное приложение на одном сервере. Добавляете сложность без какой-либо выгоды. Обычная сессионная cookie — проще, надёжнее, легче управляется.

Чувствительные данные обязательно в токене. Если бизнес-логика требует хранить что-то конфиденциальное в claims, нужен JWE. Просто добавить JWT здесь — не решение.


Итоговое сравнение: JWT vs Server Sessions

Критерий

JWT

Server Sessions

Масштабирование

★★★★★ stateless, любой инстанс

★★★ нужен shared store

Скорость верификации

★★★★★ локально, криптография

★★★ запрос к хранилищу

Отзыв токена

★★ только через blacklist/TTL

★★★★★ мгновенно

Простота

★★★ нужна система refresh токенов

★★★★★ всё из коробки

SSO поддержка

★★★★★ нативно

★★ сложно, shared store

Мобильные клиенты

★★★★★ заголовки, без cookie

★★★ cookie есть не везде


Заметили ошибку или хотите что-то добавить? Напишите в комментариях, это поможет улучшить статью.

Больше материалов о Java, Spring и backend-разработке ищите в нашем телеграм-канале Java Insider.