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 — утверждения о пользователе. Различают:
Тип | Примеры | Описание |
|---|---|---|
Зарегистрированные |
| Стандартные поля по RFC 7519 |
Публичные |
| Произвольные, но в IANA-реестре |
Приватные |
| Кастомные поля для вашего приложения |
⚠️ Важно: 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 с высокой нагрузкой это может привести к заметному увеличению трафика.
Типичные ошибки реализации
Ошибка | Последствие |
|---|---|
Алгоритм | Подпись не проверяется, токен принимается |
Не валидируется | "Вечные" токены, которые нельзя отозвать |
Не проверяется | Токены от чужого сервиса будут приняты |
Секрет хранится в коде | Компрометация всей системы при утечке репозитория |
Хранение в 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.
