Перейти к содержанию
Бэкенд и системы

JWT-токен: из чего состоит, как работает и чем отличается от Bearer

JWT часто называют «зашифрованным токеном с данными пользователя», и в этой фразе сразу две ошибки: обычный JWT подписан, а не зашифрован, и его содержимое прочитает любой, у кого есть строка токена. Ниже — устройство JWT по RFC 7519 и RFC 7515, стандартные поля, разница между JWT и Bearer, обязательные проверки на сервере и пример на стандартной библиотеке Python 3.12: выпускаем токен HS256, проверяем его и смотрим, как сервер отклоняет подделку и просроченный токен.

Автор: Опубликовано

7 мин чтенияРедакционный разборОбновлено
  • JWT
  • безопасность API
  • аутентификация
  • OAuth 2.0
  • Python
Главная мысль

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 — зашифрованный формат из пяти частей.

Строка JWT разделена на три части: header и payload в base64url раскрываются в JSON с alg, typ, sub, role, iat и exp, третья часть — подпись HMAC-SHA256
Header и payload декодирует любой, у кого есть токен. Подпись считается от строки из первых двух частей и без секретного ключа не подделывается. Схема RecallDeck

Payload: стандартные claims и что с ними делать

RFC 7519 регистрирует семь коротких имён. Ни одно не обязательно на уровне формата: какие требовать, решает приложение. Время записывается как NumericDate — число секунд с 1970-01-01 UTC, поэтому exp: 1762500900 — обычный Unix timestamp. Остальные поля произвольные: роль, идентификатор арендатора, scope. Таблица показывает, что означает каждое зарегистрированное поле и что с ним обычно делает сервер.

Два правила. Первое: payload видят все, поэтому туда нельзя класть пароли, лишние персональные данные и внутренние детали инфраструктуры. Второе: при сравнении времени RFC 7519 допускает небольшой запас на расхождение часов — «обычно не более нескольких минут». Запас в часы превращает короткоживущий токен в долгоживущий и обесценивает exp.

Зарегистрированные claims JWT (RFC 7519, раздел 4.1)
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: разные оси
ВопросJWTBearer
Что этоформат токенаспособ предъявления
СтандартRFC 7519RFC 6750
Как выглядитheader.payload.signatureAuthorization: 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, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Бэкенд и системы25 мин

36 вопросов по теме «аутентификация и безопасность backend» на собеседовании

36 вопросов по теме «аутентификация и безопасность backend» с подробными ответами, примерами, компромиссами и частыми ошибками кандидатов на техническом интервью.

36 подробных ответов