
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-токен.
Посмотреть, что в вашем токене лежит на самом деле, и не разбирать его руками можно здесь. Декодер покажет все claims списком, подсветит срок действия и скажет, истёк ли токен:
JWT: расшифровать токен
Покажет payload, claims, алгоритм и срок действия. Токен не уходит с вашего устройства
Попробовать
Только не берите для этого боевой токен с чужими данными. Декодирование происходит локально в браузере и никуда не отправляется, но сама строка уже содержит чужой email, роль или внутренний ID в открытом виде. Для отладки формата лучше взять тестовый токен или встроенный пример.
Короткий чек-лист
Если собрать всё сказанное в список того, что стоит проверить в своём проекте:
- Подпись проверяется на сервере, а не «мы же прочитали payload».
- Алгоритм задан явно в коде проверки, а не берётся из заголовка токена.
expв секундах, а не в миллисекундах, и access живёт минуты, а не месяцы.- Refresh хранится на сервере, ротируется и лежит в httpOnly-cookie.
- В payload нет ничего, что вы не готовы показать постороннему.
JWT — простой формат. Это всё тот же JSON, упакованный в Base64 и подписанный. Из-за кажущейся простоты его чаще всего и применяют неправильно, когда читаемость токена принимают за его проверенность.




