Перейти к содержимому

JWT

JWT (JSON Web Token)

В современном мире веб-приложений и микросервисов критически важно иметь механизм аутентификации, который не требует хранить состояние сессии на сервере. Классический подход с сессиями (session ID хранится в памяти сервера или в Redis) масштабируется плохо: каждый запрос требует проверки сессии в общем хранилище.

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

JWT часто используется для аутентификации и авторизации в REST API, а также в микросервисной архитектуре, где один сервис доверяет подписи другого.

Из чего состоит JWT

JWT — это строка в кодировке Base64URL, состоящая из трёх частей, разделённых точками.

header.payload.signature

Header (заголовок)

Содержит метаинформацию о токене: тип токена (JWT) и алгоритм подписи (например, HS256, RS256).

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

Payload (набор утверждений)

Содержит основные данные. Существуют стандартные поля (registered claims): iss (кто выпустил), exp (время истечения), sub (тема, обычно идентификатор пользователя), iat (время выпуска), aud (аудитория). А также можно добавлять произвольные поля (private claims): user_id, role, email.

{
  "sub": "1234567890",
  "name": "Иван Петров",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

Signature (подпись)

Создаётся путём шифрования (или хеширования) объединённых header и payload с помощью секретного ключа (для HMAC) или приватного ключа (для RSA). Подпись гарантирует, что токен не был подделан после выпуска.

Пример подписи HMAC-SHA256:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

Полный JWT выглядит примерно так:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikl2YW4gUGV0cm92Iiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.4m7_hl_n3YqX5jHqJZkQFpzvD9LxZhyKuqUgkPvRq4c

Как работает JWT-аутентификация

Исходные данные

  • Клиент (браузер, мобильное приложение) хочет получить доступ к защищённому ресурсу.
  • Сервер аутентификации (identity provider) знает секретный ключ (или пару ключей для RSA).

Типовой сценарий (парольный)

  1. Аутентификация. Клиент отправляет логин и пароль на эндпоинт /auth/login.
  2. Проверка и генерация JWT. Сервер проверяет учётные данные, генерирует JWT, подписывает его секретным ключом и возвращает клиенту.
  3. Хранение на клиенте. Клиент сохраняет JWT (обычно в localStorage, sessionStorage или в cookie с флагом HttpOnly).
  4. Запрос с JWT. При каждом запросе к защищённому API клиент добавляет JWT в заголовок Authorization: Bearer <token>.
  5. Валидация на сервере. Сервер проверяет подпись JWT, срок действия (exp) и извлекает данные пользователя. Если всё валидно — выполняет запрос.
    sequenceDiagram
    participant Client as Клиент
    participant Auth as Сервер аутентификации
    participant API as API сервер

    Client->>Auth: POST /login (логин, пароль)
    Auth->>Auth: Проверка пароля, генерация JWT
    Auth-->>Client: JWT токен
    Client->>API: GET /orders + Authorization: Bearer <JWT>
    API->>API: Проверка подписи JWT, извлечение user_id
    API-->>Client: Данные заказа
  

Access и Refresh токены

Проблема одного токена

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

Решение: пара токенов

Используют два токена с разным сроком жизни и разными ответственностями.

ТипСрок жизниГде хранитьНазначение
Access TokenКороткий (15 минут)В памяти приложения (React state, mobile variable)Доступ к API
Refresh TokenДлинный (7‑30 дней)HttpOnly cookie или защищённое хранилищеПолучение нового Access Token

Процесс:

  1. При логине сервер выдаёт оба токена.
  2. Клиент хранит Refresh Token в безопасном месте (обычно в HttpOnly cookie, которая недоступна JavaScript).
  3. Для API-запросов клиент использует Access Token (из памяти). Истекает быстро.
  4. Когда Access Token истекает, клиент отправляет Refresh Token на специальный эндпоинт /auth/refresh.
  5. Сервер проверяет Refresh Token, и если он валиден (и не отозван), выдаёт новый Access Token (и, возможно, новый Refresh Token).
    sequenceDiagram
    participant Client as Клиент
    participant Auth as Сервер аутентификации
    participant API as API сервер

    Client->>Auth: POST /login
    Auth-->>Client: access_token (15 мин) + refresh_token (7 дней)

    Client->>API: Запрос с access_token (валиден)
    API-->>Client: Успех

    Note over Client: access_token истек
    Client->>Auth: POST /refresh (refresh_token)
    Auth->>Auth: Проверка refresh_token
    Auth-->>Client: Новый access_token

    Client->>API: Запрос с новым access_token
    API-->>Client: Успех
  

Безопасность Refresh Token

  • Refresh Token должен быть длинным, случайным и храниться в HttpOnly cookie (JavaScript не имеет доступа).
  • Сервер может вести список отозванных Refresh Token (на случай компрометации).
  • Refresh Token может быть одноразовым (при обновлении выдаётся новый).

Где и как хранить JWT на клиенте

Место храненияДоступность для JSУстойчивость к XSSУстойчивость к CSRFРекомендация
localStorage / sessionStorageДаНизкая (XSS крадёт токен)СредняяНе рекомендуется для sensitive данных
HttpOnly cookieНетВысокая (JS не читает)Требуется защита (SameSite, CSRF-токены)Рекомендуется для Refresh Token
Cookie с флагом SameSite=StrictНетВысокаяВысокая (не отправляется на другие сайты)Лучший вариант для токенов

Современная практика:

  • Access Token хранить в памяти приложения (React state, переменная в модуле). При перезагрузке страницы токен теряется, но это решается через Refresh Token.
  • Refresh Token хранить в HttpOnly cookie с атрибутами Secure, HttpOnly, SameSite=Strict.

Подписи: HMAC vs RSA

АлгоритмПринципГде используетсяПлюсыМинусы
HS256 (HMAC)Симметричный (один секретный ключ)Внутри одного сервиса (монолит, один бэкенд)Прост в реализации, быстрыйКлюч нужно хранить в безопасности и распространять между сервисами
RS256 (RSA)Асимметричный (приватный ключ для подписи, публичный для проверки)Микросервисная архитектура (сервис аутентификации подписывает, другие проверяют публичным ключом)Не нужно доверять каждому сервису секретМедленнее, сложнее управление ключами

Для RS256:

  • Приватный ключ хранится только у сервера аутентификации.
  • Все остальные сервисы получают публичный ключ и могут проверить подпись, не имея секрета.

Отзыв (инвалидация) JWT

JWT хорош тем, что не требует хранения состояния. Но это же и его слабость: если токен украден, сервер не может его “отозвать” до истечения срока.

Механизмы отзыва:

Чёрный список (token blacklist)

Сервер хранит в Redis (или БД) множество отозванных токенов (по идентификатору jti Claim). При каждом запросе проверяется, не попал ли токен в чёрный список. Недостаток: теряется главное преимущество (stateless), и требуется общее хранилище.

Короткий срок жизни + Refresh Token

Делаем Access Token очень коротким (1–5 минут), а Refresh Token храним в HttpOnly cookie. При компрометации Access Token злоумышленник имеет доступ только в течение 1–5 минут, а Refresh Token недоступен из JavaScript.

Версионирование пользователя

Храним в БД версию токена (например, token_version для пользователя). При отзыве увеличиваем версию, а в JWT включаем version. Сервер проверяет version из JWT с версией в БД. Требует одного запроса к БД, но не хранит список токенов.

Размер JWT

JWT — текстовый (Base64URL), поэтому может быть большим. При включении многих полей полезной нагрузки размер превышает 1 КБ, что на каждый запрос увеличивает трафик. Не включайте в JWT большие объекты, списки прав (можно использовать scope вместо списка). Микросервисы должны получать права отдельно (по user_id из JWT).

Что должен знать аналитик о JWT

  • JWT — самодостаточный токен, не требующий обращения к БД при каждом запросе (stateless). Это упрощает горизонтальное масштабирование, но усложняет отзыв токенов.
  • JWT состоит из header, payload и signature. Signature гарантирует, что токен не подделан.
  • Access Token живёт коротко (минуты), Refresh Token — долго (дни). Refresh Token используется только для получения нового Access Token.
  • Хранить Refresh Token следует в HttpOnly cookie (безопасно от XSS). Access Token можно держать в памяти приложения.
  • Для отзыва токенов используют чёрные списки, короткое время жизни Access Token или версионирование пользователя.
  • В микросервисной архитектуре предпочтительнее RS256: сервис аутентификации подписывает токен приватным ключом, а остальные сервисы проверяют публичным.