
JWT-токен: что внутри, как его расшифровать и как работает авторизация
ДБ
Дмитрий Борисенко
Фронтенд получает от бэкенда 401, хотя пользователь только что залогинился. Или наоборот: токен, который должен был протухнуть час назад, всё ещё пропускают. В обоих случаях первый вопрос один и тот же — а что вообще лежит внутри этой длинной строки, которую браузер таскает в каждом запросе?
Хорошая новость: JWT читается без всяких ключей, за пару секунд. Плохая — именно из-за этой лёгкости вокруг него накопилось много опасных заблуждений. Разберёмся по порядку: что внутри токена, как на нём держится авторизация и где эта схема чаще всего ломается.
Если токен уже под рукой, его можно расшифровать прямо в браузере — вставляете строку целиком, header и payload раскладываются в читаемый JSON, без единого запроса на сервер.
Из чего состоит JWT-токен
JWT (JSON Web Token) — это строка из трёх частей, разделённых точками:
Каждая из первых двух частей — обычный JSON, закодированный в Base64URL.
Заголовок (header) говорит, каким алгоритмом подписан токен и что это вообще за формат:
Полезная нагрузка (payload) — сами данные: кто это, какие роли, до какого момента токен годен. Отдельные поля здесь называют claims («утверждениями»):
Подпись (signature) — результат хеширования первых двух частей секретным ключом. Данных она не содержит; её задача — доказать, что токен выдали именно вы и с тех пор его не правили.
Кстати, обычный Base64 на JWT спотыкается. Токены кодируют в Base64URL —
варианте, где + и / заменены на - и _, а хвостовые = отброшены, чтобы
строку можно было класть в URL и HTTP-заголовки без экранирования. Если скормить
такую строку обычному кодировщику Base64, он выдаст
либо ошибку, либо мусор — нужен URL-safe режим.
Почему JWT — это не шифрование
Здесь кроется главная путаница: Base64 — это кодирование, а не шифрование. У
кодирования нет ключа, и раскодировать его может кто угодно за долю секунды —
ровно то же самое проделывает atob() в консоли браузера. Подставьте любой
чужой JWT (например, из примера выше) в декодер — увидите его payload целиком,
без всякого пароля.
Из этого следует практическое правило: в payload JWT нельзя класть ничего, что не должно быть публично читаемым. Токен защищает не содержимое, а целостность: подпись не даёт незаметно подменить данные внутри, но не скрывает их.
Стандартные claims: что означают sub, exp и остальные
Часть полей payload определена спецификацией RFC 7519, и большинство библиотек понимают их из коробки:
| Claim | Значение | Пример |
|---|---|---|
sub |
Subject — кому выдан токен | ID пользователя |
iss |
Issuer — кто выдал токен | auth.example.com |
aud |
Audience — для какого сервиса | api.example.com |
exp |
Expiration — когда токен истекает | Unix-время в секундах |
iat |
Issued At — когда токен выдан | Unix-время в секундах |
nbf |
Not Before — с какого момента год | Unix-время в секундах |
jti |
JWT ID — уникальный ID токена | UUID |
Времена хранятся как Unix-время (секунды с 1 января 1970 года), а не как строка
с датой — поэтому в сыром JSON вместо 2026-08-03 вы увидите что-то вроде
1785715200. Декодер переводит такие поля в читаемую дату сам. Остальные claims
произвольные: их состав задаёт конкретное приложение — роль, права, тариф.
Как работает авторизация по JWT
Схема укладывается в четыре шага:
- Пользователь отправляет логин и пароль.
- Сервер проверяет их, собирает payload (кто это, какие роли, до какого времени токен годен) и подписывает своим секретом. Получившуюся строку отдаёт клиенту.
- Клиент кладёт токен в заголовок каждого следующего запроса:
Authorization: Bearer eyJhbGci... - Сервер пересчитывает подпись. Сошлась — значит токен настоящий, данные из payload можно использовать, в базу за пользователем ходить не нужно.
Вот в четвёртом пункте и кроется вся привлекательность подхода. Классическая сессия — это ключ от шкафчика: сервер хранит у себя, кому какой выдал. JWT — это пропуск с печатью: вся информация написана на нём самом, достаточно проверить, что печать не подделана. Поэтому JWT удобен там, где серверов много и общего хранилища сессий у них нет: микросервисы, несколько бэкендов за балансировщиком, публичное API.
За это удобство приходится платить, и цена вполне конкретная: отозвать выданный JWT нельзя. Сервер не помнит, что он его выдавал, — значит и забыть не может. Уволили сотрудника, у пользователя угнали токен, поменяли роли — все ранее выданные пропуска будут исправно работать до самого истечения срока.
Access и refresh: зачем нужны два токена
Отсюда и растёт схема с двумя токенами — она существует ровно для того, чтобы обойти проблему выше.
Access token — рабочая лошадка. Ходит в каждом запросе, живёт мало: обычно от 5 до 30 минут. Если его украдут, окно для злоупотребления получается коротким.
Refresh token — долгожитель, живёт днями или неделями. Он не даёт доступа к данным, у него единственная задача: когда access протухнет, обменять себя на свежую пару. И вот его-то сервер хранит у себя в базе — а значит, может в любой момент удалить.
Так и получается компромисс: быстрая проверка без похода в базу для 99% запросов и при этом возможность мгновенно вышибить пользователя из системы — достаточно удалить его refresh, и через несколько минут, когда истечёт текущий access, доступ закончится.
Практические детали, которые обычно всплывают уже в бою:
- Refresh нужно ротировать. При каждом обмене выдаём новый refresh, старый помечаем использованным. Если старым попытались воспользоваться второй раз — почти наверняка его скопировали, и всю цепочку токенов этого пользователя стоит аннулировать.
- Класть refresh в
localStorage— плохая идея. Любой XSS на странице прочитает его одной строкой JavaScript. Безопаснее httpOnly-cookie: браузер её отправит, а скрипт до неё не доберётся. - Параллельные запросы. Если у вас на странице пять запросов сразу и все получают 401, наивная реализация отправит пять запросов на обновление токена. Нужна очередь: первый обновляет, остальные ждут его результат.
Расшифровать токен — не значит проверить его
Это самая дорогая ошибка во всей теме, и звучит она в коде примерно так:
Формально код работает: payload прочитан, роль извлечена. Фактически это дыра
размером с ворота. Никто не мешает злоумышленнику взять свой честный токен,
подменить в payload "role": "user" на "role": "admin", перекодировать в
Base64URL и отправить. Подпись при этом станет невалидной — но её же никто и не
смотрит.
Декодирование — это разбор Base64, ключ не нужен, сделать может кто угодно. Верификация — это пересчёт подписи секретным ключом, и только она отвечает на вопрос «а этот токен вообще наш?».
Отсюда простое правило: на клиенте декодировать можно и нужно (показать имя
пользователя, понять, не пора ли обновлять токен), но любое решение о доступе
принимается только на сервере и только после проверки подписи. Библиотеки для
этого есть под каждый язык — в Node это jsonwebtoken или jose, в Python
PyJWT, в Go golang-jwt.
По той же причине онлайн-декодер (и наш в том числе) честно не пытается подтвердить, что подпись настоящая: для этого нужен секретный ключ, которого нет ни у браузера, ни у любого стороннего сервиса. Вопрос «что лежит в токене» ключа не требует, а на вопрос «валиден ли он» может ответить только тот, кто его выдал.
Отдельная классика — атака alg: none. Спецификация JWT когда-то допускала
токены без подписи, и наивные библиотеки честно их принимали: подменяешь
заголовок на {"alg":"none"}, отрезаешь подпись — и токен проходит проверку.
Современные библиотеки такое отбивают, но проверять алгоритм явно, а не доверять
тому, что написано в самом токене, — до сих пор хорошая привычка.
Алгоритмы подписи: HS256 и RS256
В поле alg заголовка чаще всего встречаются два семейства:
- HS256 (HMAC + SHA-256). Один и тот же секретный ключ и подписывает токен, и проверяет подпись. Проще в настройке, но ключ должен знать каждый сервис, который проверяет токены.
- RS256 (RSA + SHA-256). Токен подписывается приватным ключом, а проверяется публичным. Приватный ключ остаётся у одного сервиса-эмитента (например, Auth0 или собственного auth-сервера), а публичный можно спокойно раздать всем, кто должен проверять токены — подделать подпись без приватного ключа они всё равно не смогут.
Для микросервисной архитектуры RS256 обычно удобнее: сервисам, которые только проверяют токены, не нужно доверять секрет, которым можно подписать новый.
Почему токен «истёк» раньше времени
Три claim'а отвечают за время — iat, exp и nbf, — и все три хранят
Unix-время в секундах. Отсюда две регулярные ошибки.
Миллисекунды вместо секунд. В JavaScript Date.now() возвращает
миллисекунды, а JWT ждёт секунды. Если написать
exp: Date.now() + 3600000, получится токен со сроком годности примерно до
55000 года — и он будет работать вечно, чего вы как раз и не хотели.
Правильно так:
Рассинхрон часов. Если сервер, выдающий токены, и сервер, их проверяющий,
разошлись по времени на пару минут, свежевыданные токены будут отлетать как «ещё
не активные» (это как раз про nbf). Большинство библиотек позволяют задать
допуск clockTolerance в несколько секунд — это нормальная практика, а не
костыль.
Что нельзя класть в payload
Раз payload читается кем угодно, список получается коротким и жёстким.
Нельзя: пароли (даже хеши), номера карт, паспортные данные, внутренние секреты и ключи API. Всё это прочитает любой, кому токен попадётся в руки.
Нежелательно: email и телефон — формально не катастрофа, но это персональные данные, и они утекут вместе с логами, где токены сохраняются целиком чаще, чем хотелось бы.
Осторожно с объёмом: токен едет в заголовке каждого запроса. Список из двухсот прав доступа в payload — это лишние килобайты на каждом обращении к API, а некоторые прокси-серверы просто режут слишком длинные заголовки. Разумный payload — идентификатор пользователя, роль, время жизни. Всё остальное сервер и так возьмёт из базы, когда понадобится.
Если токен не декодируется
- Токен не разбивается на три части. Проверьте, что скопировали строку целиком — обрезанный при копировании конец обычно и есть причина.
- Ошибка в JSON после декодирования. Значит, в токен закралась опечатка или лишний символ — токены не редактируют вручную, их перевыпускает сервер.
- Это вообще не JWT. Строка из одной части без точек — просто Base64 или токен доступа другого формата (например, opaque-токен), а не JWT.
Посмотреть, что в вашем токене лежит на самом деле, и не разбирать его руками можно здесь — декодер покажет все claims списком, подсветит срок действия и скажет, истёк ли токен:
JWT: расшифровать токен
Покажет payload, claims, алгоритм и срок действия. Токен не уходит с вашего устройства
Попробовать
Только не берите для этого боевой токен с чужими данными. Декодирование происходит локально в браузере и никуда не отправляется, но сама строка уже содержит чужой email, роль или внутренний ID в открытом виде — для отладки формата лучше взять тестовый токен или встроенный пример.
Короткий чек-лист
Если собрать всё сказанное в список того, что стоит проверить в своём проекте:
- Подпись проверяется на сервере, а не «мы же прочитали payload».
- Алгоритм задан явно в коде проверки, а не берётся из заголовка токена.
expв секундах, а не в миллисекундах, и access живёт минуты, а не месяцы.- Refresh хранится на сервере, ротируется и лежит в httpOnly-cookie.
- В payload нет ничего, что вы не готовы показать постороннему.
JWT — простой формат: это всё тот же JSON, просто упакованный в Base64 и подписанный. Именно из-за кажущейся простоты его чаще всего и применяют неправильно — принимая читаемость токена за его проверенность.




