JWT (JSON Web Token)
JWT (JSON Web Token)
JWT — это компактный формат подписанной «бумажки», в которой сервер записывает
факты о клиенте (кто он, до какого времени валиден, что ему можно), отдаёт её
клиенту и потом верит ей без запроса в базу.
История
JWT родился как побочная ветка длинного разговора про идентичность в вебе.
В нулевых сессия обычно жила на сервере: пользователь логинился, сервер
записывал ему session_id в память или таблицу, клиент таскал этот id в
куке. Работает, пока сервер один. Как только серверов становится десять,
приходится либо клеить пользователя к одному узлу (sticky sessions), либо
класть сессии в общий Redis. И то, и другое — операционная боль.
Параллельно рос OAuth. Первая версия (2007) была про делегирование доступа,
но в ней токены были непрозрачными строками — сервер каждый раз ходил к
провайдеру и спрашивал «а этот токен ещё жив?». Медленно.
В 2010–2013 разные компании начали делать одно и то же: «а давайте
запишем данные о пользователе прямо в токен и подпишем его, чтобы
не бегать никуда». У Google было своё, у Microsoft своё. Чтобы это
работало между системами, нужен был общий формат.
Формат родил рабочая группа IETF JOSE (JSON Object Signing and
Encryption). Основные авторы — Mike Jones (Microsoft, автор
множества стандартов OpenID/OAuth) и John Bradley, при активном
участии Nat Sakimura (OpenID Foundation). Финальный документ —
RFC 7519, опубликован в мае 2015 года. Одновременно вышло
семейство: RFC 7515 (JWS — подпись), RFC 7516 (JWE — шифрование),
RFC 7517 (JWK — формат ключа), RFC 7518 (JWA — набор алгоритмов).
JWT — только «профиль» JWS для нужд аутентификации.
Практическое распространение обеспечил OpenID Connect (2014, до
финального RFC), который построен поверх OAuth 2.0 и использует JWT
для id_token. Именно поэтому «войти через Google» технически — это
почти всегда получить JWT.
Дальше JWT ушёл далеко за пределы веба: в мобильные приложения, в
межсервисную аутентификацию внутри микросервисов, в API-ключи облачных
провайдеров, в подписи webhook-ов. К 2020 году это стал одним из
самых узнаваемых форматов в индустрии — и одновременно самым
критикуемым: за десять лет накопилось много историй, как его
неправильно проверяли.
Что это такое
Технически JWT — это строка из трёх частей, разделённых точками:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlBhc2hhIn0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Первая часть — header (заголовок), обычно содержит тип (JWT) и
алгоритм подписи (HS256, RS256, ES256). Вторая — payload
(полезная нагрузка), обычный JSON-объект с полями (их называют
claims — «утверждения»). Третья — signature (подпись), MAC или
цифровая подпись первых двух частей.
Каждая часть кодируется в base64url — это обычный base64, но с
безопасными для URL символами (- и _ вместо + и /) и без
паддинга. Кодирование, важно, — не шифрование. Payload можно
раскодировать и прочитать любым: перейди на jwt.io,
вставь токен — увидишь всё, что там лежит.
Смысл трёх частей такой: header и payload — открытая информация,
подпись — доказательство, что их выдал именно тот, у кого есть
секретный ключ. Меняешь один байт в payload — подпись перестаёт
сходиться, токен ломается.
Ключевой момент, который отличает JWT от «сессии» в классическом
понимании: сервер не хранит его. Всё, что нужно для проверки —
секретный ключ (для HMAC) или публичный ключ (для RSA/ECDSA). Токен
приходит, сервер пересчитывает подпись, сравнивает — если сошлось и
exp не истёк, значит верим. Никаких походов в базу.
JWT отличается от непрозрачного токена (opaque token). Opaque —
это просто случайная строка, которая сама по себе ничего не
означает; смысл ей придаёт таблица на сервере. Пропало соединение с
базой — токены не работают. JWT самодостаточен. Плата за это —
токен нельзя «отозвать» так же легко: он валиден, пока не истёк.
JWT отличается от сессионной куки. Кука — это транспортный
механизм (браузер сам кладёт её в каждый запрос), а JWT — формат
данных. JWT можно возить в куке, в заголовке Authorization: Bearer,
в query-параметре, в теле POST. И наоборот, в куке может лежать что
угодно, не обязательно JWT.
Аналогии из жизни
Бумажный пропуск с печатью. Ты пришёл в бизнес-центр, тебе выдали
пропуск: «Иван Петров, до 18:00, этаж 5», внизу — печать охраны.
Охранник на 5-м этаже не звонит вниз проверять — он смотрит на печать
(её невозможно подделать) и на время (18:00 ещё не наступило). Пропуск
и есть JWT.
Где ломается: пропуск украли — и вор попадает на этаж 5, потому
что подпись всё ещё валидна. Именно поэтому в JWT ставят короткий
exp. И именно поэтому в важных системах пропуск проверяют не только
по печати, но и по фото — а в вебе к JWT прикручивают вторую проверку:
IP, отпечаток браузера, второй фактор.
Билет в кино. Ты купил онлайн, тебе прислали QR-код. Внутри QR —
все данные: фильм, зал, ряд, место, время сеанса, плюс подпись
кинотеатра. Билетёр сканирует, приложение говорит «валидный» — и
пропускает. Никаких звонков в кассу, вся правда внутри билета.
Где ломается: скриншот билета можно переслать другу. Он придёт
раньше тебя — сканер пикнет «валидный» и пустит. Кинотеатры решают это
через jti (уникальный идентификатор билета) плюс список «уже
использованных» — тот самый компромисс, из-за которого чистый JWT
не так уж и stateless. В веб-версии это блэклист отозванных токенов.
Заверенная нотариусом доверенность на автомобиль. Ты приехал на
чужой машине к гаишнику, показываешь бумагу: «Я, Владелец, разрешаю
Ивану ездить до 31 декабря». Гаишник не звонит владельцу — он проверяет
печать нотариуса (публичный ключ) и дату (exp). Если сходится — едешь
дальше. Именно так работают межсервисные JWT, подписанные ключом
identity-провайдера: сервис-получатель не знает лично identity-провайдера,
но знает его публичный ключ.
Где ломается: если владелец передумал — забрать доверенность у Ивана
уже невозможно. Она валидна до 31 декабря, и никакой отзыв через
интернет не поможет, если гаишник не подключён к базе отозванных
доверенностей. Отсюда — идея коротких JWT и refresh-токенов.
Как это работает
Шаг за шагом, на примере логина в веб-приложение.
Шаг 1. Клиент отправляет логин/пароль. POST на /auth/login с
{email, password}.
Шаг 2. Сервер проверяет пароль. Достаёт хеш из БД, сравнивает.
Если всё в порядке — переходит к выпуску токена.
Шаг 3. Сервер собирает payload. JSON-объект с claims:
{
"sub": "user_42",
"email": "pasha@example.com",
"role": "admin",
"iat": 1785840000,
"exp": 1785843600
}
sub (subject) — кто, iat (issued at) — когда выдан, exp
(expiration) — до какого момента валиден (обычно 15 минут — 1 час).
Есть и другие стандартные claims: iss (issuer, кто выдал),
aud (audience, кому предназначен), nbf (not before, не валиден до),
jti (уникальный ID). Плюс любые свои поля.
Шаг 4. Сервер собирает header.
{"alg": "HS256", "typ": "JWT"}
alg — алгоритм подписи. typ — тип.
Шаг 5. Кодирование в base64url.
header_b64 = base64url(header_json)
payload_b64 = base64url(payload_json)
Шаг 6. Подпись. Считается MAC или цифровая подпись от
header_b64 + "." + payload_b64:
signature = HMAC_SHA256(secret_key, header_b64 + "." + payload_b64)
signature_b64 = base64url(signature)
Шаг 7. Склейка. Три части через точки:
header_b64.payload_b64.signature_b64 — это и есть JWT. Отдаём
клиенту в ответе (обычно в JSON: {"access_token": "..."}).
Шаг 8. Клиент хранит токен. Обычно в памяти (переменная в JS),
localStorage или в куке с флагами HttpOnly; Secure; SameSite.
Шаг 9. Клиент шлёт запрос. В заголовке:
Authorization: Bearer eyJhbGciOi...
Шаг 10. Сервер проверяет токен. Разбирает три части, пересчитывает
подпись с тем же секретом — сравнивает. Если сошлось, читает payload и
проверяет exp (текущее время меньше exp?). Если оба ответа «да» —
запрос принят, sub — это пользователь, role: admin — можно делать
админские вещи.
Никаких обращений к базе, кроме собственно бизнес-логики. В этом и
сила: сервис аутентификации может стоять отдельно, выпустить токен и
уйти в отпуск — а сервисы, принимающие токен, продолжают работать сами.
Для асимметричных алгоритмов (RS256, ES256) картинка немного
другая: сервер, выпускающий токен, держит приватный ключ, а все
сервисы-приёмники знают только публичный. Публичный обычно
раздаётся через JWKS-эндпоинт (.well-known/jwks.json) — JSON со
списком ключей, которые провайдер сейчас использует. Приёмник забирает
эту раздачу, кэширует, проверяет подписи. Это позволяет провайдеру
менять ключи (rotate) без необходимости всем всё пересылать.
Где встречается в обычной жизни
- Кнопка «Войти через Google/Apple/ВКонтакте» — то, что прилетает
твоему приложению после нажатия, этоid_tokenв формате JWT
(стандарт OpenID Connect). Внутри — твой email, имя, аватарка, до
какого времени валиден. - QR-коды билетов (концерты, самолёты, метро в некоторых странах) —
часто это JWT или очень похожая конструкция: полезные данные плюс
подпись, которую сканер проверяет офлайн. - Ссылки «сбросить пароль» — токен в URL обычно тоже JWT, где
внутриsub: user_id,purpose: password_reset,exp: +1 час.
Именно поэтому такие ссылки истекают. - Приглашения в Slack/Telegram/Notion — уникальная ссылка часто
содержит JWT с идентификатором приглашения и правами. - Push-уведомления от Apple/Google — провайдер требует JWT для
доступа к своему API отправки уведомлений.
Где встречается в IT и бизнесе
- Микросервисы. Основной способ передать «контекст пользователя»
между сервисами без общей сессионной базы. Auth-сервис выпустил —
все остальные читают. - API-ключи облаков (Google Cloud, Firebase, некоторые эндпоинты
AWS). Ты подписываешь короткий JWT своим приватным ключом сервисного
аккаунта — облако его принимает. - Webhook-ы. Отправитель кладёт JWT в заголовок, получатель
проверяет подпись — гарантия, что вызов пришёл от известного
источника, а не подделан. Так делают Slack, GitHub, некоторые
платёжные системы. - SSO в корпоративных системах. SAML долгое время держал этот
рынок, но JWT/OpenID Connect его сильно потеснили — они проще и
дешевле в интеграции. - Мобильные приложения — стандартный способ хранения сессии на
устройстве: access-token (короткий JWT) плюс refresh-token
(длинный, обычно opaque) в защищённом хранилище.
Кто пользуется
Практически все крупные интернет-компании. Auth0 (сейчас часть
Okta) — исторически один из главных драйверов популяризации JWT,
специализированный сервис на этом; используют десятки тысяч клиентов.
Firebase Authentication от Google — миллионы приложений, каждый
логин отдаёт JWT. AWS Cognito — стандартный auth-сервис Amazon,
внутри JWT. Keycloak — популярный open-source identity-сервер
(изначально Red Hat), выдаёт JWT. Okta, Azure AD (сейчас
Microsoft Entra ID) — корпоративные SSO с JWT под капотом.
Из «продуктовых» примеров: GitHub Apps авторизуются в GitHub API
через подписанный своим приватным ключом JWT. Stripe использует
JWT для сессий в дашборде. Slack, Notion, Figma — везде
внутри веб-приложения токены доступа в формате JWT или его
диалекта.
По масштабам: типичный крупный сервис проверяет десятки тысяч JWT в
секунду — и это дешёвая операция (миллисекунды), потому что не
требует I/O.
Альтернативы и конкуренты
- Классические сессии (opaque session id).
Плюсы: легко отзывать, серверу можно менять данные о сессии
моментально, размер минимальный (32 байта).
Минусы: нужен общий сторидж между инстансами, лишний поход в БД на
каждый запрос. - PASETO (Platform-Agnostic Security Tokens).
Плюсы: убирает исторические грабли JWT — фиксированный алгоритм,
никакихalg: none, меньше зон для ошибок разработчика.
Минусы: намного менее распространён, мало готовых библиотек, редко
поддерживается облачными сервисами. - SAML-токены.
Плюсы: зрелый стандарт корпоративного SSO, богатый набор
возможностей, XML-подписи.
Минусы: XML, тяжеловесность, сложность интеграции, редко подходит
для мобильных приложений. - Macaroons (Google, 2014).
Плюсы: можно «сузить» уже выданный токен без обращения к серверу
(например, «этот токен, но только на чтение и только в течение часа»).
Минусы: почти никто не использует, экосистема слабая.
Когда НЕ стоит использовать
- Когда нужна мгновенная отзываемость. Если пользователь нажал
«выйти со всех устройств» и это должно сработать через секунду —
чистый JWT не подходит. Придётся либо держать блэклист (тогда зачем
вообще JWT), либо жить с окном в 15 минут до истечения текущего
токена. Для банковских систем и админок это часто неприемлемо. - Когда токен нужно возить в браузере, а данных много. JWT — это
base64url, размер растёт линейно от количества claims. Реалистичный
токен с ролями и разрешениями легко становится 2–4 КБ. Если он
ездит в куке — это оверхед на каждый запрос к тому же домену,
включая картинки и CSS. - Когда единственная причина «использовать JWT» — что это модно.
Если у тебя монолит на одном сервере, обычная сессия с id в куке
проще, надёжнее и не требует разбираться с ключами, ротацией и
правилами проверки. JWT решает конкретную проблему —
распределённость. Нет проблемы — не тащи решение.
Связанные понятия
- JWS (JSON Web Signature) — базовый стандарт подписи для JOSE,
на нём построен JWT. RFC 7515. - JWE (JSON Web Encryption) — вариант, где payload зашифрован, а
не только подписан. RFC 7516. Используется реже, чем JWS. - JWK (JSON Web Key) и JWKS — формат представления ключа и набор
ключей. Через.well-known/jwks.jsonпровайдеры отдают публичные
ключи для проверки JWT. - OpenID Connect — надстройка над OAuth 2.0, которая описывает,
как использовать JWT в качествеid_tokenпри логине через
внешний провайдер. - Refresh token — второй токен, выдаваемый вместе с JWT, живёт
долго и служит одной цели: получить новый access-JWT, когда старый
истёк, без повторного ввода пароля. - Bearer authentication — схема авторизации, при которой токен
просто кладётся в заголовокAuthorization: Bearer <token>и любой,
у кого он есть, считается владельцем. JWT почти всегда возят именно
так.
Литература и источники
- RFC 7519 «JSON Web Token (JWT)» — первоисточник, май 2015.
Читается за 40 минут, короткий и понятный. Ищи на datatracker.ietf.org. - jwt.io — интерактивная страница декодирования JWT, там же список
библиотек и подробный справочник по claims. Полезно один раз пройти
все примеры руками. - OWASP JWT Cheat Sheet — короткая, но плотная выжимка правил
безопасного применения. Начинать чтение полезно именно с неё. Искать
в Google по запросу «OWASP JSON Web Token Cheat Sheet». - «OAuth 2 in Action», Justin Richer, Antonio Sanso, 2017 (en) —
лучшая книга про OAuth в целом, JWT там разбирается на реальных
примерах. - Auth0 blog (auth0.com/blog) — компания много писала про JWT,
включая честный разбор атак и рекомендаций. Не всё актуально, но
фундаментальные материалы хорошие. - Статья «Critical vulnerabilities in JSON Web Token libraries»
Тима Макклина (2015) — исторический разбор атакиalg: noneи
algorithm confusion. Ищи по автору и названию.
Где встретилось у меня
Разбирался с API одного крупного объявлений-сайта: чтобы получить
подменный номер телефона, нужно было сначала дёрнуть один эндпоинт
(он возвращал JWT со сроком жизни в несколько секунд и «правом»
запросить конкретный лот), а потом передать этот JWT в query-параметр
второго эндпоинта. Внутри токена были стандартные claims — идентификатор
объекта, идентификатор сессии, exp. Именно эта пара «получи короткий
токен → используй его на другом эндпоинте» подсветила, зачем JWT нужен:
никакого стейта между двумя разными хостами, просто подписанная
короткоживущая бумажка.
Краткое резюме
- JWT — компактная подписанная «бумажка» с данными о клиенте, которую
сервер может проверить, не заглядывая в базу. - Формат: три части через точку — header, payload, signature; всё в
base64url; payload читаемый, подпись — гарантия целостности. - Главная сила — независимость от общего сессионного хранилища. Отсюда
массовое использование в микросервисах, мобильных приложениях,
SSO и API облачных провайдеров. - Главная слабость — нельзя мгновенно отозвать. Работает связка
«короткий access-JWT + длинный refresh-token», а критичные системы
дополнительно ведут блэклист. - Не таскай JWT туда, где хватит обычной сессии. Он решает проблему
распределённости, а не «делает систему безопаснее».