Как декодировать JWT-токен: подробное руководство для разработчиков
Ваше API только что вернуло 401 Unauthorized. Заголовок Authorization: Bearer eyJhbGciOi... выглядит нормально. Истёк ли срок действия токена, неверная ли аудитория, или эмитент сменил ключ? Ответить на этот вопрос невозможно, не прочитав, что на самом деле находится внутри токена. Чтобы декодировать JWT-токен, не нужны ни секрет, ни библиотека, ни даже сетевое соединение. JWT — это три фрагмента в кодировке base64url, соединённых точками. Декодирование — чисто механическая операция: разделить, применить base64url, вызвать JSON.parse. Никакой магии, никакой криптографии.
Это руководство шаг за шагом разбирает анатомию JWT, показывает, как декодировать токен в Node.js, Python, Go и в браузере, объясняет принципиальное различие между декодированием и проверкой подписи, на котором спотыкаются большинство команд, и перечисляет реальные сценарии сбоев, с которыми вы столкнётесь. Если нужно просто посмотреть содержимое токена прямо сейчас — переходите к нашему бесплатному JWT-декодеру. Он работает целиком в браузере, поэтому продакшн-токены никогда не покидают ваше устройство.
Декодировать ≠ проверить подпись
Прежде чем углубляться в детали, запомните главную мысль этого руководства: декодирование и проверка подписи — это две разные операции. Декодирование читает claims токена, не проверяя их подлинность. Проверка подписи доказывает, что токен не был подделан. Декодирование никогда ничего не доказывает; доказательством служит только проверка подписи. Это единственное различие отделяет работающую авторизацию от уязвимости с собственным CVE.
В практическом смысле это означает: декодирование удобно и безопасно для отладки, инспекции, чтения метаданных и логирования. Принимать решения об авторизации (доступ, права, идентичность пользователя) исключительно по декодированному токену — нельзя. На сервере всегда нужна проверка подписи библиотекой и явный allowlist алгоритмов.
Что такое JWT? (Краткая анатомия)
JSON Web Token (JWT) — это компактный URL-безопасный носитель учётных данных, описанный в RFC 7519. Он передаёт claims, то есть данные о пользователе и о самом токене, между двумя сторонами. JWT состоит из трёх частей в кодировке base64url, соединённых точками: header, payload и signature.
Вот реальный токен, разбитый по частям, чтобы было видно структуру:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← header
.
eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0 ← payload
.
4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0 ← signature
Header описывает, как подписан токен, обычно { "alg": "HS256", "typ": "JWT" }. Payload содержит claims: зарегистрированные (sub, exp, iat) плюс пользовательские (role, tenant). Signature — это криптографическое доказательство, вычисленное по header и payload, которое позволяет получателю обнаружить подделку. Base64url — это URL-безопасный вариант base64; см. наше руководство по Base64 для начинающих — десятиминутное введение.
JWT встречаются повсюду в современной аутентификации: access-токены OAuth 2.0, ID-токены OpenID Connect, учётные данные API от Auth0, Okta, Clerk, Supabase, Firebase, а также токены, передаваемые между микросервисами в service mesh. Это формат учётных данных по умолчанию последнего десятилетия.
И ещё одна строка, которую необходимо усвоить: JWT кодируются, а не шифруются. Любой, кто держит токен, может прочитать каждый claim. Подпись доказывает происхождение; она не скрывает содержимое. Этот факт диктует всё остальное: что безопасно класть в payload, почему для декодирования не нужен секрет и почему проверка подписи на сервере абсолютно обязательна.
Как работает декодирование JWT (base64url, не дешифрование)
Декодирование JWT — это не криптографическая операция. Это четыре механических шага:
- Разделить токен по
.ровно на три сегмента. - Декодировать первый сегмент через base64url и разобрать как JSON. Это header.
- Декодировать второй сегмент через base64url и разобрать как JSON. Это payload.
- Третий сегмент (signature) оставить как сырые байты. Для его проверки нужен ключ.
Это весь алгоритм. Никакая библиотека не обязательна. Любой язык с base64 и JSON-парсером способен декодировать JWT в пять строк. Наш кодировщик/декодер Base64 выполнит шаги 2 и 3 вручную, если хотите увидеть механику своими глазами.
Что такое base64url?
Base64url — это обычный base64 с тремя поправками, чтобы вывод был безопасен в URL и HTTP-заголовках: - заменяет +, _ заменяет /, а замыкающие = (padding) отбрасываются. Если подать сырой base64url в стандартный base64-декодер без обратной замены символов, получится либо мусор, либо ошибка. Расширенное руководство по Base64 подробно разбирает крайние случаи с padding.
| Стандартный base64 | base64url | |
|---|---|---|
| Алфавит | A-Z a-z 0-9 + / | A-Z a-z 0-9 - _ |
| Padding | Обязателен = в конце | Отбрасывается |
| URL-безопасен? | Нет | Да |
| Пример | PDw/Pz8+ | PDw_Pz8- |
И ещё одна важная вещь, которую стоит сказать прямо: подпись нельзя дешифровать на стороне клиента. Декодирование — это однонаправленное преобразование из закодированных байтов в JSON. Проверка подписи — отдельная операция, для которой нужен либо HMAC-секрет (для семейства HS), либо публичный ключ эмитента (для RS, PS, ES, EdDSA).
Почему секрет не нужен для декодирования
Потому что payload — это base64url + JSON, а не шифротекст. Секрет вступает в игру, только когда нужно доказать, что токен не был подделан, и это уже проверка подписи. Любой на сетевом маршруте, любой, кто увидит токен в строке лога, любой с браузером может прочитать каждый claim, который вы туда положили. Именно поэтому никогда не кладите в payload пароли, API-ключи или PII сверх того, что получатель уже знает. Подробнее о модели угроз и безопасности веб-приложений — в нашем руководстве по основам веб-безопасности.
Декодировать JWT онлайн в три клика: бесплатный JWT-декодер
Иногда ответ нужен прямо сейчас. Истёк ли этот токен, тот ли aud в нём указан, нет ли в header alg:none? Самый быстрый путь — наш онлайн-декодер JWT. Он построен под путь incident response в два часа ночи.
- Вставьте полный токен в поле ввода. Включая все три сегмента, разделённых точками.
- Прочитайте декодированный header, payload и статус-чипы вверху: алгоритм, время выпуска, время истечения и красный бейдж
Истёк, еслиexpуже в прошлом. - Скопируйте нужную панель в bug-report, тред Slack или тестовую фикстуру.
Почему это безопасно для реальных продакшн-токенов:
- 100% работа в браузере. Декодирование выполняется через нативные
atobиJSON.parse. Никаких сетевых запросов, никогда. - Никакого логирования, никакой аналитики, никаких cookie, никакой регистрации.
- Работает офлайн после загрузки страницы.
Декодер JWT не привязан к алгоритму. Поскольку для декодирования нужны лишь base64url и JSON, он читает любой вариант JWS: HS256/384/512, RS256/384/512, PS256/384/512, ES256/384/512, EdDSA и alg:none. От алгоритма зависит только проверка подписи, и проверять подпись на публичной веб-странице не следует. Подробности — чуть ниже.
Нужно вручную base64-декодировать сегмент для перепроверки? Используйте наш кодировщик/декодер Base64 и подавайте каждый сегмент как base64url.
Как декодировать JWT в коде (Node.js, Python, Go, браузер)
Для всего, что не является интерактивной отладкой, — middleware, тесты, миграционные скрипты, CLI-инструменты — вы возьмёте библиотеку. Ниже — минимальный код для декодирования JWT в четырёх средах, в которые вы попадёте чаще всего, с путями «только декодирование» и «полная проверка» рядом. Каждый сниппет можно скопировать и запустить; результат соответствует комментариям.
Декодирование JWT в Node.js (jsonwebtoken)
// npm install jsonwebtoken
const jwt = require('jsonwebtoken');
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' +
'.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0' +
'.4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0';
// Только декодирование — НЕ проверяет подпись
const decoded = jwt.decode(token, { complete: true });
console.log(decoded.header); // { alg: 'HS256', typ: 'JWT' }
console.log(decoded.payload); // { sub: 'user_123', exp: 1999999999 }
// Проверка — путь для продакшна
const secret = process.env.JWT_SECRET;
const verified = jwt.verify(token, secret, { algorithms: ['HS256'] });
Всегда передавайте явный allowlist algorithms в verify. Его пропуск исторически позволял злоумышленникам понижать RS256-токен до HS256, подписывая публичным ключом как HMAC-секретом — классическая algorithm-confusion атака. Allowlist — ваша защита.
Декодирование JWT в Python (PyJWT)
# pip install PyJWT
import jwt
token = (
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
".eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0"
".4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0"
)
# Только декодирование — небезопасно для авторизации, но подходит для просмотра
decoded = jwt.decode(token, options={"verify_signature": False})
print(decoded) # {'sub': 'user_123', 'exp': 1999999999}
# Header без касания payload
header = jwt.get_unverified_header(token)
print(header) # {'alg': 'HS256', 'typ': 'JWT'}
# Проверка — путь для продакшна
payload = jwt.decode(
token,
key="your-hs256-secret",
algorithms=["HS256"],
audience="api.example.com",
)
PyJWT отказывается выполнять проверку без списка algorithms — разумное поведение по умолчанию, защищающее от той же confusion-атаки, о которой предупреждал пример на Node.
Декодирование JWT в Go (golang-jwt/jwt/v5)
// go get github.com/golang-jwt/jwt/v5
package main
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
".eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0" +
".4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0"
// Только декодирование
parser := jwt.NewParser()
claims := jwt.MapClaims{}
_, _, err := parser.ParseUnverified(tokenString, claims)
if err != nil {
panic(err)
}
fmt.Println(claims["sub"], claims["exp"]) // user_123 1.999999999e+09
// Проверка
secret := []byte("your-hs256-secret")
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected alg: %v", t.Header["alg"])
}
return secret, nil
})
fmt.Println(token.Valid, err)
}
Замыкание keyFunc — это место, где вы навязываете семейство алгоритмов. Отклоняйте всё, что не соответствует ожидаемому методу, до того как вернёте ключ.
Декодирование JWT в браузере (без зависимостей)
Иногда зависимость не нужна вовсе: быстрая отладочная панель, расширение для браузера, маленький UI-бейдж с ролью текущего пользователя. Хватает нативных API браузера.
function decodeJwt(token) {
const [h, p] = token.split('.');
const pad = (s) => s + '==='.slice((s.length + 3) % 4);
const decodeSegment = (s) => {
const b64 = pad(s).replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes));
};
return { header: decodeSegment(h), payload: decodeSegment(p) };
}
const { header, payload } = decodeJwt(token);
console.log(header); // { alg: 'HS256', typ: 'JWT' }
console.log(payload); // { sub: 'user_123', exp: 1999999999 }
Шаг с TextDecoder важен для любого токена с не-ASCII payload (эмодзи в имени, кириллица в preferred_username). Простой atob возвращает бинарную строку, на которой JSON.parse ломается из-за многобайтового UTF-8. Именно так работает наш онлайн-декодер JWT локально в вашем браузере — только без UI.
Сводная таблица
| Язык | Только декодирование | Проверка | Библиотека |
|---|---|---|---|
| Node.js | jwt.decode(token) | jwt.verify(token, key, { algorithms: [...] }) | jsonwebtoken |
| Python | jwt.decode(token, options={"verify_signature": False}) | jwt.decode(token, key, algorithms=[...]) | PyJWT |
| Go | parser.ParseUnverified(token, claims) | jwt.Parse(token, keyFunc) | golang-jwt/jwt/v5 |
| Браузер | atob + TextDecoder + JSON.parse | Запрашивайте у бэкенда | — |
Декодирование и проверка подписи: критическое различие
Декодирование JWT читает его claims; проверка JWT доказывает, что эти claims не были подделаны. Декодирование никогда не доверяет; доверие — это проверка. Это единственное различие, которое отделяет работающую реализацию аутентификации от появления CVE.
| Декодирование | Проверка подписи | |
|---|---|---|
| Нужен секрет/ключ? | Нет | Да |
| Запускается на стороне клиента? | Безопасно | Никогда |
| Доказывает подлинность? | Нет | Да |
| Проверяет срок действия? | Опционально | Да |
| Сценарий | Отладка, инспекция | Аутентификация, авторизация |
Никогда не принимайте решение об авторизации по декодированному (но не проверенному) JWT. Ни в middleware, ни в React-хуке, ни в serverless-функции, которая, как вам кажется, спрятана за gateway. Декодированные claims говорят о том, что утверждает токен; проверенные claims говорят о том, что подписал эмитент. Атакующий, передающий вашему серверу собственноручно собранный токен без валидной подписи, может положить в payload что угодно, и только проверка подписи это отсеет.
Ещё одна деталь про семейство HMAC: если используете HS256, энтропия секрета — это всё, что у вас есть. Короткий, угадываемый секрет подбирается офлайн против любого перехваченного токена, и далее атакующий выпускает свои собственные токены и заходит через парадную дверь. Используйте минимум 256 бит настоящей случайности. Подробный материал по силе HMAC-секретов и арифметике brute-force разобран в руководстве по энтропии паролей.
Справочник распространённых JWT claims
В каждом JWT, который встретится, используется какое-то подмножество зарегистрированных claims из RFC 7519. Запомните короткий список:
| Claim | Назначение | Пример | Замечания |
|---|---|---|---|
iss | Issuer | https://auth.example.com | Кто выпустил токен |
sub | Subject | user_123 | Обычно идентификатор пользователя |
aud | Audience | api.example.com | Кому предназначен токен — должен совпасть на сервере |
exp | Expiration | 1715003600 | Unix-секунды; в прошлом = истёк |
iat | Issued At | 1715000000 | Unix-секунды выпуска токена |
nbf | Not Before | 1715000060 | Самое раннее время, когда токен валиден |
jti | JWT ID | d1f8… | Уникален на токен; защищает от replay |
kid | Key ID (header) | key-2025-01 | Какой ключ из вашего JWKS подписал токен |
Рядом с ними сидят прикладные claims: role, scope, email, tenant_id — всё, что выпускает ваш identity provider. Держите их короткими. Каждый байт едет с каждым запросом.
Чтобы прочитать человекочитаемые даты из iat и exp, попробуйте наш конвертер Unix timestamp. Вставьте число — получите дату в локальном часовом поясе и поймаете баг рассинхронизации часов за секунду.
Troubleshooting: почему мой JWT не декодируется?
Пять реальных режимов отказа, в порядке убывания частоты. Каждый описан в формате Симптом → Причина → Исправление.
- «Invalid JWT format, expected three segments». Вы скопировали только payload, или shell перенёс токен на несколько строк, и вы ухватили только первую. Исправление: скопируйте полное значение
xxx.yyy.zzzиз исходного тела ответа, а не из перерисованного терминала. Длинные однострочные значения лучше выживают во вкладке Network браузерных devtools, чем в прокрученном терминале. - Пять сегментов вместо трёх. У вас JWE (зашифрованный JWT), а не JWS. Формат —
header.encryptedKey.iv.ciphertext.tag. Декодер прочитает header, но payload — это шифротекст. Исправление: декодирование payload требует ключа дешифрования, обычно его обрабатывает на сервере ваш auth-SDK, а не отладочный инструмент. - Ошибка base64url на токене, который выглядит валидно. Где-то по пути копирования (cookie, redirect URL, перехваченный лог прокси) токен был URL-encoded. Вы увидите буквальные
%2Eили%2Bв строке. Исправление: URL-декодируйте сначала, потом подайте результат в JWT-декодер. - Ошибка JSON-парсинга на payload. Терминал или чат-клиент вставил soft-wrap переносы, или скрипт навязал «умные» кавычки вокруг идентификатора. Исправление: посмотрите сырые байты ответа (curl с
-o file.txt, либо Raw view в devtools), удалите пробельные символы и вставьте заново. - Декодируется чисто, но бэкенд всё равно отвергает. Это уже не проблема декодирования, а проблема проверки. Токен структурно валиден; что-то проверяемое сервером (подпись,
aud,exp, рассинхронизация часов,kid-lookup) проваливается. Переходите к следующему разделу.
Два почётных упоминания, которые не являются ошибками парсинга, но их стоит ловить, пока декодер открыт: значение alg равно none в header (в продакшне относитесь как к враждебному) и значение exp в прошлом. Декодер всё равно показывает claims, чтобы вы могли отлаживаться, — это правильное поведение, и наш инструмент помечает их красным бейджем Истёк.
Когда декодирования недостаточно: проверка подписи
Декодирование заканчивается на «вот что утверждает токен». Проверка подписи — это то, что превращает утверждение в решение о доверии. Подпись — это доказательство, вычисленное приватным ключом эмитента или общим секретом, которое связывает header и payload. Измените хотя бы один байт — и проверка провалится. Без этой проверки любой, кто может выполнить POST к вашему endpoint, способен вручную собрать «admin»-токен, отредактировав payload и пропустив подпись.
Никогда не принимайте alg:none.
Продакшн-проверка на любом языке и в любом фреймворке выглядит примерно как этот чек-лист. Считайте отсутствующие пункты багами:
- Передавайте явный allowlist
algorithms: ['RS256'](или какой используете). Это защита от algorithm-confusion атак. - Проверяйте, что
audсовпадает с идентификатором вашего сервиса, аiss— с ожидаемым URL эмитента. - Сверяйте
expс текущим временем, давая не более 60 секунд на рассинхронизацию часов. - При ротации ключей ищите публичный ключ по
kidчерез JWKS endpoint. Никогда не зашивайте один и тот же ключ навсегда. - Эффективно отзывайте токены, держа
expкоротким (минуты, не дни) и при необходимости поддерживаяjti-denylist для высокоценных токенов.
В каждой популярной JWT-библиотеке эти параметры доступны как опции одного вызова. Если ваш код проверки их не задаёт, вы оставляете значения по умолчанию, а исторически именно они были багом. Полную модель угроз и руководство по веб-безопасности см. в нашем материале по основам веб-безопасности.
FAQ
Можно ли декодировать JWT без секретного ключа?
Да. Header и payload закодированы в base64url, а не зашифрованы, поэтому любой, кто держит токен, может прочитать его claims. Секрет или публичный ключ нужен только для проверки подписи. Это сделано намеренно: payload должен быть читаемым, чтобы получатель мог принимать решения об авторизации.
Безопасно ли вставлять продакшн-JWT в онлайн-декодер?
Только если декодер работает в браузере и никогда не загружает токен. Наш JWT Decoder разбирает токен локально через нативные atob и JSON.parse; ничего не отправляется ни на какой сервер. Удалённые отладчики, которые шлют ваш токен POST-запросом, следует считать утечкой учётных данных.
В чём разница между декодированием и проверкой JWT?
Декодирование просто читает claims. Оно не требует ключа и ничего не доказывает. Проверка сверяет подпись с ключом эмитента и подтверждает, что токен не был подделан. Никогда не принимайте решение об аутентификации по декодированному, но не проверенному токену.
Мой JWT выглядит обрезанным. Что считается валидным форматом?
Валидный JWT содержит ровно три base64url-сегмента, разделённых точками: header.payload.signature. Пять сегментов означают зашифрованный JWT (JWE), а не JWS. Ноль точек значит, что вы скопировали только один сегмент из перенесённой строки терминала.
Почему декодер всё ещё показывает истёкший токен?
Декодер читает claims независимо от валидности, чтобы вы могли отлаживаться. Только верификатор отказывается принимать истёкшие токены. Наш инструмент показывает бейдж Истёк, сравнивая exp с локальными часами, поэтому проблему видно мгновенно — без вглядывания в Unix-таймстампы.
Какие алгоритмы можно декодировать?
Все. Декодированию нужны только base64url и JSON-парсинг, поэтому оно не зависит от алгоритма. Это включает HS256/384/512, RS256/384/512, PS256/384/512, ES256/384/512, EdDSA и alg:none. От алгоритма зависит только проверка подписи.
Использовать jwt-decode или jsonwebtoken в Node.js?
Используйте jwt-decode на фронтенде, когда нужно лишь прочитать payload — например, показать имя пользователя из access-токена. Используйте jsonwebtoken на бэкенде, потому что только бэкенд может держать подписывающий ключ и выполнять jwt.verify. Никогда не проверяйте на клиенте.
Заключение
Декодирование JWT не такое загадочное, как намекает выражение «криптографический токен». Пять выводов — и вы больше никогда не зависнете, разглядывая непрозрачную строку eyJhbGciOi…:
- Декодирование — это base64url плюс JSON-парс. Секрет не требуется.
- JWT состоит из трёх частей (header, payload, signature), соединённых точками.
- Декодирование никогда не доказывает подлинность. Всегда проверяйте подпись на сервере ключом эмитента.
- Отвергайте
alg:noneи всегда передавайте явный allowlist алгоритмов вverify. - Никогда не храните в payload пароли, приватные ключи или чувствительные PII. Это читаемо любым, кто держит токен.
Сохраните в закладки наш бесплатный JWT-декодер для дежурной отладки. Вставьте токен, прочитайте claims и заметьте истечение за секунду — токен ни разу не покинет браузер.