Технологии

JWT токен: что это, как устроен и как проверить

32 минАктуально на 12 сентября 2026

JWT токен: устройство и проверка

Ты вошёл в приложение, закрыл страницу, вернулся — а оно по-прежнему узнаёт тебя и разрешает записать результат игры. Часто за этим стоит JWT токен: небольшая подписанная строка, с которой клиент подтверждает свою личность при обращении к серверу. Чтобы безопасно ставить задачи Claude Code, тебе не нужно писать систему авторизации вручную, но нужно понимать, что находится в токене, чему в нём можно доверять и что нельзя класть внутрь.

Содержание
  1. Что такое JWT простыми словами
  2. Как устроен JWT токен: header, payload и signature
  3. Что такое Base64 и почему payload можно прочитать
  4. JWT токен: пример с разбором полей
  5. Как JWT токен живёт: от логина до запроса к API
  6. Access и refresh токены: зачем нужны два
  7. Где хранить токен в браузере: localStorage или httpOnly-cookie
  8. Как расшифровать и проверить JWT токен
  9. JWT в Supabase: anon key, сессия и auth.uid()
  10. Типичные ошибки новичка при работе с JWT
  11. Когда JWT не нужен
  12. Частые вопросы
  13. Короткий вывод

Что такое JWT простыми словами

JWT расшифровывается как JSON Web Token. По смыслу это компактная карточка с данными, которую одна часть системы передаёт другой. Чаще всего сервер выдаёт её после входа, а браузер прикладывает к следующим запросам. JWT токен помогает серверу понять, от имени какого пользователя пришёл запрос и какие правила к нему применить.

Удобная аналогия — пропуск с печатью. На проходной сотрудник один раз показывает документ. Охрана проверяет его и выдаёт пропуск, где написаны номер сотрудника, роль и срок действия. Дальше не нужно показывать паспорт у каждой двери: достаточно пропуска. Но дверь доверяет ему только после проверки печати и даты.

JWT работает похоже:

  • сервер проверяет логин, одноразовый код или другой способ входа;
  • создаёт набор утверждений о пользователе;
  • подписывает их своим ключом;
  • отдаёт готовую строку клиенту;
  • при новых запросах проверяет подпись и срок действия.

Подпись делает незаметную подмену данных практически невозможной без ключа. Пользователь может открыть токен и увидеть его содержимое, но не может просто заменить роль player на admin и получить действующий пропуск. После изменения хотя бы одного символа старая подпись перестанет совпадать.

Слово «токен» шире, чем JWT. Токеном может быть любая строка, подтверждающая право на действие: случайный идентификатор сессии, ключ доступа к API или одноразовый код. JWT — конкретный формат, в котором данные помещены внутрь самой строки. Поэтому не каждый токен является JWT и не каждый JWT обязательно служит для входа пользователя.

Аутентификация отвечает на вопрос «кто ты?», а авторизация — «что тебе разрешено?». Вход подтверждает личность, а роль и серверные правила определяют доступные действия. Подробнее эта связка разобрана в материале про авторизацию, OTP, сессии и токены.

Для «Змейки» JWT может быть цифровым пропуском игрока. После входа сервер выдаёт токен с идентификатором player-42. Когда игра отправляет новый рекорд, сервер узнаёт игрока по проверенному токену, а не по полю user_id, которое браузер мог бы подменить.

Как устроен JWT токен: header, payload и signature

Обычный подписанный JWT состоит из трёх частей, разделённых точками:

header.payload.signature

В готовом виде это одна длинная строка без пробелов. Каждая часть выполняет свою работу.

Header — заголовок

В заголовке записана служебная информация. Обычно там есть:

  • typ — тип токена, часто JWT;
  • alg — алгоритм, которым создана подпись, например HS256 или RS256.

До преобразования заголовок может выглядеть так:

{
  "alg": "HS256",
  "typ": "JWT"
}

Алгоритм из заголовка нельзя принимать как команду без проверки. Сервер заранее знает, какой алгоритм разрешён для конкретного издателя токена, и должен сверять значение с этим списком. Иначе злоумышленник может попытаться подсунуть токен с более слабым или отключённым способом проверки.

Payload — полезная нагрузка

В payload лежат утверждения, или claims: идентификатор пользователя, время выдачи, срок действия и другие данные. Это не доказательства сами по себе. Доверие появляется только после успешной проверки подписи, издателя, назначения и времени.

Пример содержимого:

{
  "sub": "player-42",
  "role": "player",
  "iat": 1789203600,
  "exp": 1789204500
}

JWT допускает стандартные и собственные поля. Стандартные имеют общепринятый смысл: sub обозначает субъекта, exp — момент окончания действия, iat — момент выдачи. Поле role приложение добавляет для своих правил. Названия собственных полей зависят от системы.

Signature — подпись

Подпись связывает первые две части с ключом. Упрощённо сервер берёт закодированные header и payload, соединяет их точкой и применяет криптографический алгоритм:

signature = sign(base64url(header) + "." + base64url(payload), key)

При симметричном алгоритме вроде HS256 один секретный ключ используют и для создания, и для проверки подписи. Такой ключ должен оставаться только на доверенном сервере. При асимметричном алгоритме вроде RS256 подпись создают закрытым ключом, а проверяют открытым. Открытый ключ можно раздать сервисам-проверяющим, не давая им возможности выпускать новые токены.

Подпись не скрывает payload. Её задача — показать, что содержимое не менялось после выпуска и что подпись создала сторона, владеющая нужным ключом. Для секретности данных нужно шифрование или вообще отказ от помещения таких данных в токен.

Иногда встречаются зашифрованные JWT, но типичный токен авторизации с тремя частями — подписанный, а не зашифрованный. Новичку безопаснее исходить из простого правила: всё в header и payload сможет прочитать тот, кто получит строку.

Что такое Base64 и почему payload можно прочитать

JSON содержит кавычки, фигурные скобки, кириллицу и другие символы, которые неудобно передавать внутри компактного токена. Поэтому первые две части преобразуют в Base64URL — вариант кодирования Base64, подходящий для адресов и заголовков HTTP. Он превращает байты в ограниченный набор печатных символов.

Кодирование не равно шифрованию. Для Base64URL не нужен секретный ключ. Любой человек может выполнить обратное преобразование и получить исходный JSON. Это похоже не на сейф, а на запись текста другим алфавитом: вид изменился, доступ к смыслу остался.

В JWT используется именно Base64URL. В отличие от обычного Base64, этот вариант заменяет символы + и / на - и _, а завершающие знаки = часто опускает. Поэтому простой декодер Base64 иногда требует сначала вернуть замены и дополнить строку до длины, кратной четырём.

Публичность payload приводит к практическому правилу: не клади туда то, что нельзя показывать пользователю или человеку, перехватившему токен. Внутри не должно быть:

  • пароля и его хеша;
  • секретного ключа API;
  • банковских реквизитов;
  • закрытой переписки;
  • лишних персональных данных;
  • внутреннего секрета подписи.

Идентификатор пользователя, роль и технические отметки времени обычно допустимы, если приложение действительно в них нуждается. Даже тогда лучше хранить минимум. Чем больше токен, тем больше данных передаётся с каждым запросом и тем болезненнее возможная утечка.

Почему прочитать можно, а подделать нельзя? Допустим, игрок декодировал payload и заменил:

"role": "player"

на:

"role": "admin"

После изменения получится другой набор байтов. Подпись была рассчитана для старого набора, поэтому проверка не пройдёт. Создать новую правильную подпись без серверного ключа пользователь не сможет — при условии, что алгоритм надёжен, ключ не утёк, а сервер реализовал проверку корректно.

Отсюда следует граница доверия. Браузер может читать токен, показывать имя и заранее скрывать недоступную кнопку, но окончательное решение всегда принимает сервер. Скрытая кнопка не защищает данные: запрос можно отправить вручную. Сервер обязан проверить подпись и права независимо от интерфейса.

JWT токен: пример с разбором полей

Ниже учебный JWT токен для игрока «Змейки». Он подписан демонстрационной строкой и не подходит для настоящего приложения:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJwbGF5ZXItNDIiLCJyb2xlIjoicGxheWVyIiwiaWF0IjoxNzg5MjAzNjAwLCJleHAiOjE3ODkyMDQ1MDB9.q0ATiq4GhCzZEQa_KhlM72VlXiw4H9eWuSwI4TJGKiI

Точки делят строку на три фрагмента. После декодирования первых двух частей получатся знакомые объекты JSON.

Заголовок:

{
  "alg": "HS256",
  "typ": "JWT"
}

Полезная нагрузка:

{
  "sub": "player-42",
  "role": "player",
  "iat": 1789203600,
  "exp": 1789204500
}

Разберём поля без лишней теории.

sub — кто является владельцем токена

sub — сокращение от subject, «субъект». Здесь это стабильный идентификатор игрока player-42. Сервер может использовать его, чтобы найти профиль и связать новый рекорд с правильной строкой в базе.

Не стоит подменять sub адресом почты, если системе достаточно внутреннего идентификатора. Почта меняется и раскрывает больше личной информации. Случайный UUID или другой внутренний ID обычно удобнее.

iat — когда токен выдан

iat означает issued at. Значение записано в секундах с начала эпохи Unix — от 1 января 1970 года по UTC. В примере токен выдан 12 сентября 2026 года в 09:00 UTC.

Поле помогает понять возраст токена и применять дополнительные правила. Само наличие iat не ограничивает срок жизни: для этого нужно exp и его реальная проверка.

exp — когда токен перестаёт действовать

exp означает expiration time. В примере срок заканчивается 12 сентября 2026 года в 09:15 UTC. После этого сервер должен отклонить токен, даже если подпись по-прежнему правильная.

Значение сравнивается с серверным временем. Небольшой допустимый сдвиг часов иногда учитывают, но он не должен превращаться в обход срока действия. Клиентская проверка удобна для интерфейса, однако защитой считается только проверка на сервере.

role — роль в приложении

role: "player" — собственное поле приложения. Оно может отличать обычного игрока от модератора. Но поле полезно только тогда, когда его добавил доверенный сервер и сервер же проверил подпись.

Роли требуют аккуратности при долгоживущих токенах. Если права человека изменились, старый токен может продолжать содержать прежнюю роль до окончания срока. Решение зависит от проекта: короткий срок access-токена, дополнительная проверка в базе или отзыв сессии.

В реальном JWT также встречаются:

  • iss — кто выпустил токен;
  • aud — для какого сервиса он предназначен;
  • nbf — раньше какого момента токен принимать нельзя;
  • jti — уникальный идентификатор токена.

Проверяющий сервер не должен ограничиваться красивым JSON. Он сверяет ожидаемый алгоритм, корректность подписи, exp, а при наличии правил — iss, aud, nbf и другие поля. Токен с правильной подписью, выпущенный для другого API, всё равно может быть непригоден для текущего сервиса.

Как JWT токен живёт: от логина до запроса к API

Жизненный цикл начинается не с токена, а со входа. Пользователь подтверждает личность паролем, одноразовым кодом, ссылкой из письма или через внешний сервис. Только после успешной проверки сервер создаёт сессию и возвращает токен.

Для «Змейки» последовательность выглядит так:

  1. Игрок вводит данные для входа.
  2. Сервер авторизации проверяет их.
  3. Сервер выдаёт подписанный JWT.
  4. Клиент хранит токен выбранным способом.
  5. Игра отправляет рекорд в API и прикладывает токен.
  6. API проверяет подпись, срок и допустимые поля.
  7. Проверенный sub связывается с новым рекордом.
flowchart TB
    login["Игрок входит в Змейку"] --> auth["Сервер проверяет вход"]
    auth --> issue["Сервер выдаёт JWT"]
    issue --> store["Клиент хранит токен"]
    store --> request["Игра отправляет рекорд"]
    request --> verify["API проверяет подпись и срок"]
    verify --> save["Рекорд записан для игрока"]

Когда клиент сам передаёт access-токен в API, стандартный вариант — заголовок Authorization со схемой Bearer:

Authorization: Bearer eyJhbGciOi...

Bearer означает, что право даёт сам факт владения строкой. Отправитель не доказывает отдельно, как получил её. Поэтому утёкший действующий токен может использовать другой человек до истечения срока или отзыва сессии. JWT передают только по HTTPS, не вставляют в публичные логи, скриншоты, адресную строку и сообщения.

API JWT токен обрабатывается до выполнения защищённой операции. Сервер обычно делает несколько проверок:

  • токен присутствует там, где ожидается;
  • строка имеет корректный формат;
  • алгоритм входит в разрешённый список;
  • подпись совпадает;
  • срок exp не закончился;
  • токен уже разрешено применять по nbf, если поле есть;
  • iss и aud соответствуют этому приложению;
  • роль или другие права позволяют действие.

Только после этого обработчик рекорда получает идентификатор пользователя. Поле user_id из тела запроса не должно побеждать проверенный sub. Иначе игрок сможет отправить чужой ID и записать результат не в свой профиль.

Получить JWT токен простым декодированием нельзя. Его выдаёт доверенная система после входа или другого разрешённого процесса. Если Claude Code предлагает «сгенерировать токен в браузере» с секретом подписи, это критическая ошибка архитектуры: вместе с кодом секрет получит любой посетитель сайта.

Access и refresh токены: зачем нужны два

Один долгоживущий JWT удобен, но опасен: если строка утекла, доступ сохранится надолго. Один очень короткий токен безопаснее, но пользователю пришлось бы постоянно входить заново. Пара access и refresh токенов разделяет эти задачи.

Токен Для чего нужен Обычно куда отправляется Главный риск
Access Доступ к защищённым операциям API С каждым запросом к нужному API Кража даёт доступ до истечения срока
Refresh Получение нового access-токена Только на специальный серверный адрес обновления Кража позволяет продлевать доступ дольше

Access-токен живёт сравнительно недолго. Игра отправляет его при записи рекорда, загрузке профиля и других защищённых действиях. Когда срок заканчивается, API перестаёт его принимать.

Refresh-токен предназначен не для рекордов и не для обычных запросов. Клиент отправляет его серверу авторизации, получает новую пару или новый access-токен и продолжает работу без повторного ввода данных. Refresh-токен может быть JWT, но не обязан: часто это случайная непрозрачная строка, связанная с записью на сервере.

У пары есть собственные правила безопасности:

  • refresh-токен хранят осторожнее, потому что цена его утечки выше;
  • при обновлении старый refresh-токен можно заменять новым;
  • выход из аккаунта должен завершать или отзывать сессию на серверной стороне, если система поддерживает отзыв;
  • повторное применение уже заменённого токена может быть сигналом к блокировке всей цепочки;
  • access-токен не используют после exp, даже если рядом есть действующий refresh.

JWT сам по себе не гарантирует мгновенный отзыв. Если сервер проверяет только подпись и срок, уже выданный токен останется действительным до exp. Для более строгого контроля система хранит состояние сессии, список отозванных идентификаторов или использует короткие access-токены. Получается компромисс: часть проверки выполняется без запроса к базе, но управление сессиями всё равно может потребовать серверного состояния.

Точный срок жизни нельзя выбирать по универсальной цифре. Он зависит от ценности данных, риска устройства, возможности обновления и требований продукта. Проси ИИ предложить варианты с объяснением компромиссов, а не задавать «вечный» срок ради удобства.

После входа браузеру нужно где-то держать данные сессии. Два часто обсуждаемых варианта — localStorage и cookie с флагом HttpOnly. У каждого есть сильные и слабые стороны, поэтому честный ответ начинается с архитектуры приложения и модели угроз.

localStorage

localStorage — хранилище браузера, доступное JavaScript-коду страницы. Данные переживают перезагрузку вкладки и закрытие браузера, пока приложение или пользователь их не удалит. Код может прочитать access-токен и вручную добавить его в Authorization: Bearer.

Главный риск — XSS, то есть выполнение на странице чужого JavaScript. Если злоумышленник добился запуска кода в контексте сайта, он может прочитать токен из localStorage и отправить себе. После этого строку можно применять с другого устройства, пока она действительна.

Само использование localStorage не создаёт XSS. Уязвимость появляется из-за небезопасной вставки HTML, зависимостей, обработки пользовательского ввода или других ошибок. Но доступность токена для любого выполняющегося скрипта увеличивает последствия такой уязвимости.

Cookie с HttpOnly

Cookie с флагом HttpOnly браузер отправляет серверу автоматически, но JavaScript страницы не может прочитать её через обычные браузерные API. Это снижает риск прямой кражи токена при XSS. Флаги Secure и SameSite, подходящие ограничения домена и пути дополняют защиту.

У cookie есть другой класс риска — CSRF, когда браузер автоматически прикладывает учётные данные к запросу, инициированному с чужого сайта. SameSite, проверка происхождения запроса и CSRF-токены помогают защищаться, но схему нужно проектировать целиком. Кроме того, HttpOnly не делает XSS безвредным: вредоносный код может не увидеть саму cookie, но выполнить действие от имени пользователя прямо в открытой странице.

Критерий localStorage HttpOnly-cookie
Доступ из JavaScript Да Нет
Отправка с запросом Код добавляет вручную Браузер добавляет автоматически по правилам cookie
Основная угроза для токена Кража через XSS CSRF и неверные настройки cookie
Защищает ли от всех последствий XSS Нет Нет
Удобство для API на другом домене Часто проще Нужны корректные CORS и cookie-настройки

Есть и третий вариант: хранить короткий access-токен только в памяти страницы, а обновление сессии выполнять через защищённую cookie. Тогда перезагрузка удаляет access-токен из памяти, а приложение получает новый. Схема уменьшает часть рисков, но добавляет логику обновления и не заменяет защиту от XSS и CSRF.

Нельзя честно объявить один способ лучшим для всех проектов. Для простого приложения, где сервер и страница принадлежат одному сайту, серверная сессия в HttpOnly-cookie часто получается понятнее. Для отдельного API, мобильных клиентов и нескольких доменов требования могут быть другими. Библиотека авторизации или Supabase уже задаёт значительную часть схемы — не ломай её случайным переносом токена по совету из общего ответа ИИ.

Хорошая постановка задачи Claude Code звучит конкретно:

Проверь, как сейчас хранится сессия в проекте. Опиши риски XSS и CSRF для этой схемы. Не меняй архитектуру сразу. Предложи два варианта хранения токенов, объясни влияние на CORS, выход из аккаунта и обновление сессии.

Секреты сервера, ключи подписи и токены сторонних API требуют отдельного подхода. Их нельзя смешивать с пользовательской сессией; ориентир есть в статье про хранение секретов и токенов.

Как расшифровать и проверить JWT токен

В поиске часто пишут «расшифровка JWT токена», хотя для обычного подписанного JWT точнее говорить «декодирование». Расшифровка предполагает шифр и ключ, а header и payload только закодированы в Base64URL. Проверка подписи — отдельная операция.

Перед работой сделай копию токена без последних символов или используй учебный токен. Действующий токен равен временному ключу доступа. Не вставляй рабочий токен в поисковик, публичный чат, задачу для ИИ, скриншот или чужой онлайн-декодер.

Как расшифровать JWT токен через jwt.io

На сайте jwt.io есть отладчик, который делит токен на части и показывает декодированные header и payload. Для безопасной учебной проверки достаточно вставить демонстрационный токен из примера и посмотреть на поля. Интерфейс сервиса может меняться, поэтому ориентируйся на смысл результата, а не на точное расположение кнопок.

Надпись о корректной подписи имеет смысл только в том случае, если инструмент действительно получил правильный проверочный ключ и использовал ожидаемый алгоритм. Простое появление JSON на экране не доказывает подлинность. Не передавай секрет подписи стороннему сервису и не используй реальный пользовательский токен, если правила безопасности проекта этого не допускают.

Одна строка JavaScript через atob

В консоли браузера можно декодировать payload локально. Сначала сохрани учебную строку в переменную token, затем выполни одну строку:

const payload = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(token.split('.')[1].length / 4) * 4, '=')), c => c.charCodeAt(0))));

После этого payload будет обычным объектом JavaScript. Замены превращают Base64URL в формат, понятный atob, дополнение восстанавливает пропущенные =, а TextDecoder корректно собирает UTF-8-текст.

Эта строка только читает payload. Она не проверяет подпись, не подтверждает издателя и не решает, можно ли доверять роли. Для серверной проверки нужна подходящая библиотека, ожидаемый алгоритм и ключ. Не проси ИИ писать собственную криптографию на основе сравнения строк: готовые библиотеки учитывают гораздо больше пограничных случаев.

Что проверить руками

После декодирования ответь на вопросы:

  1. Есть ли ровно три части у подписанного токена?
  2. Какой алгоритм указан в alg и разрешён ли он сервером?
  3. Кто записан в sub?
  4. Не закончился ли exp?
  5. Соответствуют ли iss и aud ожидаемой системе, если они используются?
  6. Нет ли в payload паролей, секретов и лишних личных данных?

Время exp удобно переводить в дату в консоли:

new Date(payload.exp * 1000)

Умножение на тысячу нужно потому, что JWT обычно хранит NumericDate в секундах, а конструктор Date в JavaScript принимает миллисекунды.

Что попросить у Claude Code

ИИ полезнее всего использовать не как онлайн-декодер, а как ревизора кода проекта. Дай ему задачу без реальных токенов и секретов:

Найди в проекте код выпуска и проверки JWT. Ничего не меняй. Покажи, где проверяются подпись, допустимый алгоритм, exp, iss и aud. Отдельно укажи, откуда берётся ключ и может ли он попасть в клиентский код или логи. Не выводи значения токенов и секретов.

После отчёта можно попросить исправление:

Исправь только подтверждённые проблемы проверки JWT. Используй существующую библиотеку проекта, запрети alg none, зафиксируй ожидаемый алгоритм и добавь тесты на просроченный, изменённый и выпущенный для другого audience токен. Значения секретов не печатай.

Такая формулировка отделяет диагностику от изменений и задаёт проверяемый результат. В тестах нужны как успешный случай, так и отказ: токен с изменённым sub, истёкшим exp, неверной подписью и неподходящим aud должен быть отклонён.

JWT в Supabase: anon key, сессия и auth.uid()

В проекте на Supabase ты встретишь несколько похожих строк, и их легко перепутать. Публичный anon key проекта имеет формат JWT. Он сообщает API, что запрос относится к публичной роли anon, но сам по себе не означает, что перед сервером вошедший пользователь.

anon key рассчитан на использование в клиентском приложении при условии, что доступ к таблицам защищён Row Level Security, или RLS. Его публичность не даёт права отключать политики. Совсем другая категория — серверные ключи с повышенными правами: их нельзя помещать в браузерный код.

После входа пользователя Supabase выдаёт сессию. Её access-токен тоже является JWT и уже описывает конкретного пользователя. В нём есть идентификатор субъекта и другие поля, нужные системе авторизации. Клиент Supabase прикладывает этот токен к запросам, а сервер проверяет его.

RLS-политика выполняется рядом с базой и может получить ID вошедшего пользователя через auth.uid(). Для таблицы рекордов смысл правила может быть таким: пользователь вправе вставить строку только тогда, когда user_id новой записи совпадает с auth.uid(). Тогда нельзя записать рекорд за соседа простой подменой JSON в браузере.

Упрощённый пример условия:

auth.uid() = user_id

Это не готовая политика для копирования вслепую: действие, роли, структура таблицы и проверки зависят от схемы проекта. Полный разбор находится в статье про RLS-политики Supabase.

Для «Змейки» поток выглядит так:

  • приложение создаёт клиент Supabase с публичным адресом проекта и anon key;
  • игрок входит, и клиент получает пользовательскую сессию;
  • access-токен сессии отправляется при добавлении рекорда;
  • Supabase проверяет JWT и определяет пользователя;
  • политика сравнивает auth.uid() со значением user_id;
  • подходящая запись проходит, чужая отклоняется.

Наличие anon key в исходном коде не заменяет RLS. Если таблица открыта без политик, злоумышленнику не требуется красть пользовательский JWT: он сможет обращаться к API напрямую в пределах выданных публичной роли прав. Защиту обеспечивает не сокрытие публичного ключа, а минимальные разрешения и корректные политики.

Обратная ошибка — принять anon key за токен текущего игрока. У всех посетителей приложения он один и тот же, поэтому извлечь из него auth.uid() конкретного человека нельзя. Идентичность появляется после входа и содержится в пользовательской сессии.

Не изменяй payload токена Supabase вручную и не выпускай его в браузере. Клиентская библиотека управляет сессией, а доверие строится на подписи сервиса. Если нужны собственные роли или дополнительные claims, сначала проверь поддерживаемый Supabase способ и влияние на RLS, обновление токена и отзыв доступа.

Типичные ошибки новичка при работе с JWT

Ошибки JWT редко заметны по внешнему виду приложения. Вход работает, рекорд сохраняется, интерфейс показывает имя — но защита может существовать только на экране. Ниже проверки, которые стоит явно поручить ИИ.

Секрет подписи попал в клиентский код

Если браузер сам подписывает JWT общим секретом, любой посетитель может открыть собранные файлы и достать ключ. После этого он выпустит токен с любым sub и любой ролью. Обфускация, минификация и необычное имя переменной секрет не защищают.

Клиент может хранить публичный ключ для проверки асимметричной подписи, потому что с ним нельзя создать новую подпись. Закрытый ключ или общий секрет HMAC остаётся на сервере и не попадает в репозиторий, логи и ответы API.

У токена бесконечный срок жизни

JWT без разумного срока действия превращает одну утечку в долгий доступ. Даже если пользователь сменил пароль или перестал пользоваться устройством, украденная строка может продолжить работать.

Добавление exp недостаточно, если сервер его не проверяет. Нужен тест, который создаёт просроченный токен и ожидает отказ. Конкретный срок выбирают по рискам продукта и схеме обновления, а не копируют из случайного примера.

В payload хранят чувствительные данные

Base64URL создаёт видимость непонятной строки, но любой декодер покажет JSON. Пароль, платёжные сведения, личный адрес, секреты интеграций и закрытые заметки внутри JWT считаются раскрытыми тому, у кого есть токен.

Держи payload коротким. Если API нужны актуальные данные профиля, оно может получить их из базы по sub, а не переносить весь профиль в каждом запросе.

Сервер не проверяет exp

Иногда код лишь декодирует JWT и считает наличие sub доказательством входа. Такой обработчик примет просроченный токен и, в худшем случае, токен с произвольным payload. Метод с названием decode обычно не равен методу verify.

Проверка должна завершаться ошибкой до выполнения защищённого действия. Интерфейс тоже может заранее заметить истечение срока и обновить сессию, но серверная проверка обязательна.

Разрешён alg none или алгоритм выбирает сам токен

Значение alg: "none" означает отсутствие криптографической подписи. В старых или неверно настроенных реализациях злоумышленник мог объявить такой алгоритм и добиться принятия неподписанного токена. Современная библиотека и явный список разрешённых алгоритмов должны отклонять этот вариант.

Сервер не спрашивает у непроверенного токена, как именно ему доверять. Он получает ожидаемый алгоритм из собственной конфигурации и проверяет соответствие.

Проверяют только подпись

Корректная подпись ещё не говорит, что токен выпущен для этого API и действует сейчас. Нужны временные поля и, когда архитектура их использует, проверка iss и aud. Иначе токен одного сервиса может быть ошибочно принят другим.

Доверяют user_id из запроса

Даже при хорошем JWT защита ломается, если обработчик берёт владельца записи из тела запроса. Для рекорда «Змейки» сервер должен получить ID из проверенного sub или из серверного контекста авторизации и сравнить его с правилами доступа.

Печатают токены в лог

Заголовок Authorization, ответы входа и объекты сессии могут случайно попасть в консоль, систему ошибок или журнал сервера. Доступ к логам часто шире, чем к базе ключей. Маскируй такие значения и проси ИИ проверять структуру без вывода содержимого.

Считают выход удалением строки из браузера

Удаление локального токена завершает сессию только на этом клиенте. Копия, украденная раньше, от этого не исчезает. Если продукту нужен немедленный отзыв, сервер должен поддерживать завершение сессии, ротацию refresh-токенов или другую проверку состояния.

Короткий запрос для ревизии проекта:

Проведи аудит JWT-авторизации без изменений кода. Проверь, не попадает ли ключ подписи в клиентскую сборку, есть ли exp и его серверная проверка, запрещён ли alg none, фиксированы ли alg/iss/aud, не логируется ли Authorization и берётся ли владелец записи из проверенного токена. Для каждого вывода укажи файл и строку, но не показывай секреты.

Такой аудит не заменяет тестирование, зато помогает быстро найти места, где внешне рабочая авторизация не обеспечивает реальных границ доступа.

Когда JWT не нужен

JWT популярен, но не является обязательной ступенью любого приложения. Для простого сайта с одним сервером и браузерным клиентом обычной сессии в cookie часто достаточно.

При серверной сессии после входа браузер получает случайный идентификатор. Данные пользователя и срок хранятся на сервере, а cookie содержит только этот идентификатор. При каждом запросе сервер находит запись сессии и узнаёт пользователя.

У этого подхода есть понятные преимущества:

  • сессию легко отозвать одной серверной записью;
  • роль и другие данные остаются актуальными в базе;
  • cookie может быть HttpOnly, поэтому JavaScript не читает идентификатор;
  • в браузер не передаётся payload с пользовательскими сведениями;
  • архитектура одного сайта и одного сервера остаётся простой.

Цена — обращение к хранилищу сессий и необходимость делить это состояние между несколькими экземплярами сервера. Для небольшого приложения это часто нормальный и предсказуемый компромисс.

JWT становится полезнее, когда токен должны проверять несколько сервисов, есть отдельные API и клиенты или система авторизации уже построена на подписанных токенах. Асимметричная подпись позволяет сервисам проверять токен открытым ключом без доступа к закрытому ключу выпуска.

Но даже в распределённой системе JWT не освобождает от проектирования сессий. Нужно обновлять доступ, отзывать украденные refresh-токены, менять ключи, учитывать смену ролей и ограничивать область применения. Формат решает задачу переносимого подписанного набора утверждений, а не всю безопасность приложения.

Для первой «Змейки» с локальным рекордом авторизация вообще не нужна. Когда появляется общая таблица рекордов и профили, можно использовать готовую систему Supabase. Строить собственный выпуск JWT ради обучения невыгодно: сложность и цена ошибки выше пользы. Проси Claude Code подключать проверенный механизм проекта, а не изобретать формат токена заново.

Выбор можно свести к трём вопросам:

  1. Кто выпускает токен и кто должен его проверять?
  2. Нужна ли автономная проверка в нескольких сервисах?
  3. Как система немедленно завершит украденную или закрытую сессию?

Если ответы сводятся к «один сайт, один сервер, одна база», серверная cookie-сессия остаётся сильным базовым вариантом. JWT стоит выбирать по требованиям, а не потому, что это знакомое слово из примеров.

Частые вопросы

Можно ли расшифровать JWT токен без секретного ключа?

Можно декодировать header и payload, потому что они записаны в Base64URL, а не зашифрованы. Проверить подпись без нужного секрета или открытого ключа нельзя. Декодированный JSON сам по себе не заслуживает доверия.

Можно ли изменить payload JWT токена?

Технически строку можно декодировать, поменять и собрать заново. Но подпись перестанет соответствовать содержимому, и правильно настроенный сервер отклонит токен. Если изменённый токен принимается, проблема находится в серверной проверке.

Как получить JWT токен для API?

Его получают через предусмотренный API процесс: вход пользователя, OAuth-обмен, сервисную авторизацию или другой способ из документации конкретной системы. Нельзя безопасно выпустить доверенный пользовательский JWT в браузере, спрятав там общий секрет. Актуальные тарифы и условия доступа смотри на сайте сервиса.

JWT токен и API-ключ — это одно и то же?

Нет. API-ключ часто является постоянной непрозрачной строкой, связанной с приложением или проектом. JWT содержит набор утверждений и подпись, обычно имеет ограниченный срок и может описывать пользователя. Конкретный сервис иногда оформляет свой публичный ключ как JWT, как anon key в Supabase, но назначение всё равно определяется документацией.

Нужно ли удалять JWT после выхода?

Клиент должен удалить доступные ему данные сессии, а серверная система — завершить или отозвать сессию, если она поддерживает отзыв. Одного удаления из браузера недостаточно для уже украденной копии токена. Короткий срок access-токена ограничивает окно риска.

Безопасно ли хранить JWT в localStorage?

Это допустимый вариант в некоторых архитектурах, но токен доступен JavaScript и может быть украден при XSS. HttpOnly-cookie снижает риск прямой кражи, зато требует защиты от CSRF и корректных настроек cookie. Выбор зависит от устройства приложения, доменов и модели угроз.

Короткий вывод

JWT токен — это подписанный цифровой пропуск, а не сейф. Его header и payload может прочитать любой владелец строки; доверие обеспечивает проверка signature, допустимого алгоритма, срока и назначения токена.

Для «Змейки» токен связывает проверенного игрока с запросом на запись рекорда. Клиент передаёт access-токен, сервер проверяет его и берёт ID пользователя из доверенного контекста. Секрет подписи остаётся на сервере, чувствительные данные не попадают в payload, а просроченный или изменённый токен отклоняется.

Если систему авторизации пишет Claude Code, ставь ему задачу не «добавить JWT», а описать полный жизненный цикл: выпуск, хранение, передачу, проверку, обновление и отзыв. А если приложению достаточно простой cookie-сессии или готовой авторизации Supabase, собственный JWT-слой только добавит места для ошибок.

Читай дальше

Все статьи

Не просто статьи — тебя доведут до результата

В практикуме за 2499 ₽ рядом живая команда практикующих разработчиков и маркетологов: ведём по шагам до твоего работающего приложения. Не «ролики и сам разбирайся» — помогаем на каждом затыке.

Перейти к практикуму
Все статьи Ещё: технологии и архитектура