JWT
JWT (JSON Web Token)
В современном мире веб-приложений и микросервисов критически важно иметь механизм аутентификации, который не требует хранить состояние сессии на сервере. Классический подход с сессиями (session ID хранится в памяти сервера или в Redis) масштабируется плохо: каждый запрос требует проверки сессии в общем хранилище.
JWT (JSON Web Token) — это открытый стандарт (RFC 7519), который определяет компактный и самодостаточный способ передачи информации между сторонами в виде JSON-объекта. Этот объект может быть подписан (цифровая подпись) и/или зашифрован. JWT позволяет серверу проверить подлинность токена и извлечь из него данные о пользователе без обращения к общему хранилищу.
JWT часто используется для аутентификации и авторизации в REST API, а также в микросервисной архитектуре, где один сервис доверяет подписи другого.
Из чего состоит JWT
JWT — это строка в кодировке Base64URL, состоящая из трёх частей, разделённых точками.
header.payload.signatureHeader (заголовок)
Содержит метаинформацию о токене: тип токена (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).
Типовой сценарий (парольный)
- Аутентификация. Клиент отправляет логин и пароль на эндпоинт
/auth/login. - Проверка и генерация JWT. Сервер проверяет учётные данные, генерирует JWT, подписывает его секретным ключом и возвращает клиенту.
- Хранение на клиенте. Клиент сохраняет JWT (обычно в localStorage, sessionStorage или в cookie с флагом HttpOnly).
- Запрос с JWT. При каждом запросе к защищённому API клиент добавляет JWT в заголовок
Authorization: Bearer <token>. - Валидация на сервере. Сервер проверяет подпись 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 |
Процесс:
- При логине сервер выдаёт оба токена.
- Клиент хранит Refresh Token в безопасном месте (обычно в HttpOnly cookie, которая недоступна JavaScript).
- Для API-запросов клиент использует Access Token (из памяти). Истекает быстро.
- Когда Access Token истекает, клиент отправляет Refresh Token на специальный эндпоинт
/auth/refresh. - Сервер проверяет 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: сервис аутентификации подписывает токен приватным ключом, а остальные сервисы проверяют публичным.