JWT — формат, Bearer — способ предъявления. Подпись защищает целостность, но не скрывает данные. На сервере проверяйте подпись только разрешёнными алгоритмами и ключами из своей конфигурации, затем exp, nbf, iss и aud, а проблему отзыва решайте коротким сроком жизни access-токена и refresh-токеном, который сервер может отозвать.
Три части через точку
Компактный JWT выглядит как xxxxx.yyyyy.zzzzz. Первая часть — заголовок (header): JSON с алгоритмом подписи alg и типом typ. Вторая — полезная нагрузка (payload) с claims, то есть утверждениями о пользователе и о самом токене. Третья — подпись. Все три части закодированы в base64url: это Base64 с символами - и _ вместо + и / и без завершающих =, чтобы токен без экранирования проходил в URL и HTTP-заголовках.
Подпись вычисляется не от JSON, а от строки «base64url(header).base64url(payload)», поэтому любое изменение первых двух частей делает её недействительной. Обратное тоже важно: base64url — кодирование, а не шифрование. Запросы «jwt расшифровка» и «jwt декодер» решаются одной функцией декодирования без всякого ключа; ключ нужен только для проверки подписи. Не вставляйте боевые токены в сторонние онлайн-декодеры: это действующие учётные данные. Если содержимое нужно скрыть, используют JWE — зашифрованный формат из пяти частей.
Payload: стандартные claims и что с ними делать
RFC 7519 регистрирует семь коротких имён. Ни одно не обязательно на уровне формата: какие требовать, решает приложение. Время записывается как NumericDate — число секунд с 1970-01-01 UTC, поэтому exp: 1762500900 — обычный Unix timestamp. Остальные поля произвольные: роль, идентификатор арендатора, scope. Таблица показывает, что означает каждое зарегистрированное поле и что с ним обычно делает сервер.
Два правила. Первое: payload видят все, поэтому туда нельзя класть пароли, лишние персональные данные и внутренние детали инфраструктуры. Второе: при сравнении времени RFC 7519 допускает небольшой запас на расхождение часов — «обычно не более нескольких минут». Запас в часы превращает короткоживущий токен в долгоживущий и обесценивает exp.
| Claim | Что означает | Что проверяет сервер |
|---|---|---|
| iss | кто выпустил токен | совпадает с доверенным издателем |
| sub | о ком токен | субъект известен и активен |
| aud | для кого токен | в списке есть этот сервис |
| exp / nbf | окно действия | текущее время внутри окна |
| iat | когда выпущен | не из будущего, не слишком старый |
| jti | уникальный ID токена | нет в списке отозванных |
Подпись: HS256 против RS256 и ES256
HS256 — это HMAC с SHA-256 и общим секретом. Кто может проверить такой токен, тот может и выпустить его, поэтому HS256 уместен, когда выпускает и проверяет токены один сервис или тесно связанная группа сервисов. Ключ должен быть не короче выхода хеш-функции, то есть не меньше 256 бит для HS256 (RFC 7518), а использовать как ключ запоминаемый пароль RFC 8725 прямо запрещает.
RS256 (RSA) и ES256 (ECDSA на кривой P-256) асимметричны: сервер авторизации подписывает закрытым ключом, а сервисы проверяют открытым и выпустить токен не могут. Открытые ключи обычно публикуют набором JWK, а параметр kid в заголовке подсказывает, каким ключом подписан токен, — это упрощает ротацию. Правило выбора: много независимых потребителей или внешние клиенты — асимметричная подпись; один сервис — HS256 с длинным случайным секретом.
Пример: выпускаем и проверяем HS256 без библиотек
Код использует только стандартную библиотеку Python 3.12 и фиксированное «текущее время», чтобы вывод был воспроизводимым. Первая строка вывода — токен, начинающийся с eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9: это просто base64url от {"alg":"HS256","typ":"JWT"}. Вторая — payload, прочитанный без ключа: {'sub': '42', 'role': 'user', 'iat': 1762500000, 'exp': 1762500900}. Третья — тот же словарь после успешной проверки подписи и срока.
Затем мы меняем в payload роль user на admin и оставляем старую подпись: проверка печатает rejected: bad signature. Исходный токен в момент 1762501000, то есть после exp, даёт rejected: expired. Обратите внимание на две детали: алгоритм сверяется с allow-list до проверки подписи, а подписи сравниваются через hmac.compare_digest, время работы которого не выдаёт, в каком байте подписи разошлись. Это учебный код: в проекте берите поддерживаемую библиотеку и явно передавайте ей список алгоритмов, аудиторию и издателя.
import base64, hashlib, hmac, json
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def unb64url(part: str) -> bytes:
return base64.urlsafe_b64decode(part + "=" * (-len(part) % 4))
def mac(key: bytes, signing_input: str) -> bytes:
return hmac.new(key, signing_input.encode(), hashlib.sha256).digest()
def sign(payload: dict, key: bytes) -> str:
head = b64url(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode())
body = b64url(json.dumps(payload, separators=(",", ":")).encode())
return f"{head}.{body}.{b64url(mac(key, f'{head}.{body}'))}"
def verify(token: str, key: bytes, now: int) -> dict:
head, body, sig = token.split(".")
if json.loads(unb64url(head)).get("alg") != "HS256":
raise ValueError("unexpected alg") # allow-list, а не доверие заголовку
if not hmac.compare_digest(mac(key, f"{head}.{body}"), unb64url(sig)):
raise ValueError("bad signature")
claims = json.loads(unb64url(body))
if claims["exp"] <= now: raise ValueError("expired")
return claims
key = b"demo-secret-do-not-use-in-prod-32b" # в проде: secrets.token_bytes(32)
token = sign({"sub": "42", "role": "user", "iat": 1762500000, "exp": 1762500900}, key)
head, body, sig = token.split(".")
print(token, json.loads(unb64url(body)), verify(token, key, now=1762500100), sep="\n")
forged = b64url(unb64url(body).replace(b'"user"', b'"admin"'))
for t, now in [(f"{head}.{forged}.{sig}", 1762500100), (token, 1762501000)]:
try: verify(t, key, now)
except ValueError as e: print("rejected:", e)JWT и Bearer — ответы на разные вопросы
Bearer описан в RFC 6750 и отвечает на вопрос «как предъявить токен»; кодировку и содержимое самого токена RFC 6750 не задаёт. Bearer-токеном может воспользоваться любой его обладатель, как билетом без имени. Рекомендуемый способ передачи — заголовок Authorization: Bearer и токен через пробел. Передавать такие токены в URL RFC 6750 не советует: адреса оседают в истории браузера и логах серверов.
JWT отвечает на вопрос «как устроен токен». Bearer-токен может быть JWT, а может быть непрозрачной случайной строкой, которую API проверяет запросом к серверу авторизации (introspection, RFC 7662). И наоборот, JWT бывает не только bearer-токеном: ID-токен OpenID Connect тоже JWT, но предназначен клиенту, а не API. Поэтому на вопрос «JWT или Bearer?» точный ответ — чаще всего и то и другое: JWT в роли bearer-токена.
| Вопрос | JWT | Bearer |
|---|---|---|
| Что это | формат токена | способ предъявления |
| Стандарт | RFC 7519 | RFC 6750 |
| Как выглядит | header.payload.signature | Authorization: Bearer … |
| Встречается без другого | ID-токен OpenID Connect | непрозрачный токен |
Проверка на сервере, отзыв и хранение в браузере
RFC 8725 собирает типовые ошибки. Самые известные — alg: none, когда библиотека принимает токен без подписи, и подмена RS256 на HS256: сервер, который берёт алгоритм из заголовка, проверяет HMAC, используя открытый RSA-ключ как секрет, а этот ключ известен всем. Отсюда порядок проверки:
JWT проверяется без обращения к базе, поэтому до истечения exp его нельзя просто выключить. Обычный компромисс — короткоживущий access-токен и долгоживущий refresh-токен, который предъявляется только серверу авторизации и который этот сервер может отозвать. Для публичных клиентов RFC 9700 требует либо привязывать refresh-токен к ключу клиента (sender-constrained), либо менять его при каждом обновлении (ротация). В браузере localStorage доступен любому скрипту страницы, а значит и XSS; cookie с флагом HttpOnly скрипту недоступна, но тогда нужна защита от CSRF, например SameSite.
- алгоритм берётся из allow-list в конфигурации, а не из заголовка токена, и каждый ключ используется ровно с одним алгоритмом;
- ключ ищется по kid среди своих доверенных ключей, а адреса jku и x5u из токена не загружаются вслепую — только из allow-list;
- подпись проверяется до чтения любых claims;
- exp и nbf сверяются с запасом не больше нескольких минут;
- iss и aud точно совпадают с ожидаемыми значениями;
- typ различает виды токенов, чтобы один нельзя было подсунуть вместо другого.
Как ответить на собеседовании и что повторить
Короткий ответ на «что такое JWT»: подписанный контейнер claims из трёх base64url-частей; данные читаются без ключа, подпись гарантирует, что их не меняли. Дальше назовите проверки на сервере и проблему отзыва. Частые уточнения — как разлогинить пользователя, где хранить токен в SPA, чем опасен HS256 при многих потребителях, что такое атака alg: none. Все они вытекают из механики выше, так что отдельно заучивать ответы не нужно.
Для повторения подойдут подборки вопросов по аутентификации и безопасности backend и по HTTP и REST API; в RecallDeck такие вопросы удобно повторять карточками по расписанию.
Коротко
Частые вопросы
Можно ли расшифровать JWT без ключа?
Header и payload — да: это обычный base64url, никакого шифрования. Без ключа нельзя другое — изменить данные так, чтобы подпись осталась верной. Если содержимое должно быть скрыто, нужен JWE, зашифрованный формат, либо непрозрачный токен, данные которого лежат на сервере.
Чем access-токен отличается от refresh-токена?
Access-токен предъявляют API в каждом запросе, поэтому он живёт недолго. Refresh-токен предъявляют только серверу авторизации, чтобы получить новый access-токен; он живёт дольше, и сервер может его отозвать. В OAuth 2.0 refresh-токены описаны в RFC 6749.
Как разлогинить пользователя, если JWT нельзя отозвать?
Удалите или пометьте отозванным его refresh-токен на сервере: новый access-токен он не получит. Текущий access-токен доживёт до exp, поэтому срок держат коротким. Если ждать нельзя, ведут список отозванных jti до истечения exp, а при компрометации ключа меняют ключ подписи, что аннулирует все токены сразу.
Зачем в заголовке JWT поле kid?
kid — подсказка, каким ключом подписан токен. Она нужна при ротации, когда старый и новый ключи какое-то время действуют одновременно. Сервер ищет kid только среди собственных доверенных ключей и не загружает ключ по адресу из самого токена (jku, x5u), если этого адреса нет в заранее заданном allow-list.
Источники
Источники и редакционная политика
Сведения проверены 7 ноября 2026 г. Ссылки рядом с разделами указывают источники фактов и технических объяснений. Выводы, учебные сценарии и рекомендации по подготовке — редакционная работа RecallDeck.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.