JWT (JSON Web Token) — компактный токен для передачи данных между клиентом и сервером. Разберём структуру, подпись, хранение и риски.

JWT (JSON Web Token) — это компактный токен-«пропуск», который сервер выдаёт пользователю после входа, а затем клиент отправляет обратно при каждом запросе. Так сервер понимает, кто вы и что вам разрешено, не заставляя вводить пароль снова.
JWT чаще всего используют для двух вещей:
Важно: JWT не хранит «сессию» на сервере в привычном смысле. Сервер проверяет токен и принимает решение. При этом в реальных системах дополнительные проверки по базе/кэшу всё равно могут понадобиться (например, для отзыва доступа).
JWT особенно удобен там, где много запросов и нужно каждый раз «приносить» подтверждение входа с собой:
JWT сам по себе не является «магией безопасности». Это формат передачи утверждений о пользователе, защищённый подписью. Без правильных сроков жизни токенов, безопасного хранения на клиенте, корректной проверки подписи и продуманного отзыва JWT легко создаёт ложное ощущение защищённости.
Дальше разберём устройство токена, алгоритмы подписи и практики, которые делают JWT действительно безопасным.
JWT часто путают со «способом логина», но на практике он чаще решает другую задачу: как безопасно и удобно передавать результат входа между клиентом и сервером при последующих запросах.
Аутентификация отвечает на вопрос: «Кто вы?» — например, вход по паролю, коду из SMS или через соцсеть.
Авторизация отвечает на вопрос: «Что вам можно?» — доступ к профилю, изменение данных, просмотр счетов, вход в админку и т. д.
JWT обычно находится на стыке этих этапов: после успешной аутентификации сервер выдаёт токен, который помогает подтверждать личность и права в следующих запросах.
JWT — это строка, которую клиент прикладывает к запросам (обычно в заголовке Authorization). Внутри — claims (утверждения), например идентификатор пользователя и роли.
Сервер получает токен, проверяет его подлинность и понимает:
Допустим, токен содержит роль user. Тогда запрос к «Мой профиль» пройдёт.
А раздел «Админка» потребует роль admin. Если такого права в токене нет, сервер вернёт отказ — даже если пользователь «в целом вошёл».
Пользователь проходит вход.
Сервер проверяет данные и выдаёт JWT.
Клиент сохраняет токен и отправляет его с каждым запросом к API.
Сервер валидирует токен и решает, пускать ли пользователя к ресурсу.
Оба подхода решают одну задачу — «как понять, кто перед нами», — но делают это по-разному: сессии опираются на состояние на сервере, а JWT переносит часть контекста в сам токен.
При классических сессиях после логина сервер создаёт запись сессии (память, Redis, база) и выдаёт клиенту идентификатор. Обычно этот идентификатор хранится в cookie.
Важно: cookie здесь — лишь контейнер для ID, а все ключевые данные (кто пользователь, права, срок) остаются на сервере.
JWT — «самодостаточный» токен: внутри есть данные (claims) и подпись. Сервер получает токен и проверяет подпись, срок действия и нужные поля.
Идея проста: сервер доверяет не клиенту, а криптографической подписи. Если токен не подделан и не истёк — запрос можно обрабатывать.
Сессии проще отзывать: удалили запись на сервере — и дальнейшие запросы с этим ID недействительны. Это удобно для «выйти со всех устройств», блокировки аккаунта, экстренного отзыва доступа.
JWT удобен, когда много сервисов должны проверять вход без общего хранилища сессий. Но отзыв сложнее: если токен выдан на час, то без дополнительной логики он останется валидным весь час.
Сессии часто выигрывают в типичных веб‑приложениях (особенно монолитах):
JWT обычно оправдан, когда вы строите API для нескольких клиентов/сервисов и хотите дешёвую проверку на каждом запросе без постоянного обращения к хранилищу сессий.
JWT обычно выглядит как строка с тремя частями:
header.payload.signature
Каждая часть закодирована в Base64URL, чтобы токен было удобно передавать в HTTP‑заголовках и URL. Важно: Base64URL — не шифрование, а только «упаковка».
Header описывает, какой это токен и как он подписан. Чаще всего там:
typ: тип, обычно JWT (иногда опускают)alg: алгоритм подписи, например HS256 или RS256Payload содержит claims — утверждения о пользователе и самом токене. Часто встречаются:
sub — идентификатор пользователяexp — срок действияiat — когда выданaud, iss — для кого токен и кем выпущенКлючевой момент: payload не является секретом. Его легко прочитать (достаточно декодировать Base64URL), поэтому не кладите туда пароли и чувствительные данные.
Signature — криптографическая подпись, которая доказывает, что header и payload не меняли по дороге. Если кто-то подменит роль в payload (например, user на admin), подпись перестанет сходиться, и сервер должен отклонить токен.
Claims — это набор полей внутри payload JWT. Они описывают контекст: кто пользователь, для какого сервиса выдан токен, когда он действителен и какие ограничения применяются.
Обычно срок жизни задают через exp (короткий для access‑токена). На практике часы разных узлов могут расходиться — это clock skew. Поэтому при валидации часто допускают небольшой «зазор» в несколько минут.
{
"iss": "https://auth.example",
"sub": "user_12345",
"aud": "api://orders",
"exp": 1735689600,
"iat": 1735686000,
"nbf": 1735686000,
"scope": "orders:read orders:write",
"jti": "b7b1f2c2-6c2a-4a8a-9e3d-1a2b3c4d5e6f"
}
Здесь scope задаёт разрешения, а jti (JWT ID) помогает точечно отзывать токены или выявлять повторное использование.
JWT часто путают с «шифрованным токеном». На практике большинство JWT — это подписанные токены (JWS), а не зашифрованные (JWE).
Если вам нужно скрывать содержимое токена от клиента/логов/прокси — рассматривайте JWE, но учитывайте, что это усложняет обмен ключами и отладку.
HS256 использует один общий секрет для подписи и проверки.
RS256 (RSA) и ES256 (ECDSA) разделяют роли:
Это удобно для микросервисов и внешних интеграций: проверяющих много, а «подписывающий» один.
Ключи — «главный пароль» вашей системы токенов.
JWT имеет смысл только тогда, когда сервер умеет его строго проверять. До успешной валидации нельзя доверять ни sub, ни ролям, ни любым данным из payload.
Обычно токен приходит в заголовке Authorization: Bearer <token>. Сервер проверяет, что токен похож на JWT (три части, разделённые точками) и что он не пустой/не повреждён.
Сервер должен:
alg);Если подпись не сходится — токен поддельный или ключ неверный, запрос отклоняется.
После подписи проверяют «временные» поля:
exp — токен просрочен → отказ;nbf — токен ещё не должен работать → отказ;iat — помогает оценить «свежесть» (и отлавливать аномалии).Практика: допускают небольшой clock skew, иначе пользователи будут получать случайные ошибки из‑за расхождения часов.
Чтобы токен не принимался «не тем» сервисом, сервер сверяет:
iss — кто выпустил токен;aud — для какой аудитории/сервиса он предназначен.Если aud не совпадает с вашим API, токен нельзя принимать даже при валидной подписи.
Клиенту лучше отвечать нейтрально: 401 Unauthorized (иногда с WWW-Authenticate) без подробностей.
В логах полезно фиксировать причину (просрочен, неверная подпись, неверный aud/iss), но не логировать токен целиком.
JWT — это строка. На клиенте ключевой вопрос: где её держать, чтобы не отдать злоумышленнику и не сломать UX. Универсально «идеального» места нет — всегда компромисс между XSS, CSRF и удобством.
Плюсы: JavaScript не может прочитать токен, если cookie помечена как HttpOnly. Это снижает ущерб от XSS.
Минусы: cookie автоматически уходит с запросами к вашему домену — значит, появляется риск CSRF.
Чтобы снизить CSRF‑риски, обычно используют:
SameSite=Lax (часто достаточно для классических сайтов)SameSite=Strict (строже, но может ломать некоторые сценарии)Secure (только по HTTPS)Плюсы: токен не сохраняется на диск и не переживает закрытие вкладки — это уменьшает шанс «долгой жизни» украденного токена.
Минусы: при перезагрузке страницы токен исчезает — почти всегда нужен refresh‑механизм или повторный вход.
Для SPA это часто хороший компромисс: access token держат в памяти, а обновление делают через более защищённый канал (например, refresh в cookie).
Плюсы: просто реализовать, токен переживает перезагрузку.
Минусы: при XSS злоумышленник может прочитать localStorage и унести токен. Это одна из самых частых причин утечек токенов в браузерных приложениях.
HttpOnly Secure cookie + SameSite + HTTPS.Access token и refresh token используют парой, потому что они решают разные задачи безопасности и удобства.
Access token нужен для доступа к API «прямо сейчас». Если его украдут, злоумышленник получит доступ на время жизни токена.
Refresh token служит только для получения нового access token. Он не должен давать доступ к данным напрямую — только к эндпоинту обновления. Так можно сделать access token короткоживущим, не заставляя пользователя постоянно логиниться.
Типичный подход:
Хорошая практика — ротация refresh token: при каждом обновлении выдаёте новый refresh token, а старый делаете недействительным.
Если старый refresh token внезапно пришёл второй раз — это сигнал компрометации. Частая реакция: инвалидировать текущую цепочку refresh‑токенов и попросить заново войти.
JWT удобно проверять без состояния, но отзыв (logout, смена пароля, блокировка аккаунта, утечка) почти всегда требует серверной памяти:
jti/версии сессии до истечения срокаНа практике чаще контролируют именно refresh token (а access просто коротко живёт).
OAuth 2.0 и OpenID Connect (OIDC) часто упоминают рядом с JWT, поэтому создаётся ощущение, будто «OAuth = JWT». Это не так:
Если хочется быстро разложить всё по полочкам, полезно сначала освежить базу: /blog/oauth2-oidc-osnovy.
В OAuth 2.0 формат access token не строго задан: провайдер может выдавать как JWT, так и «непрозрачную» строку (opaque token), которую нужно проверять через сервер провайдера.
В OIDC, наоборот, ID Token почти всегда является JWT, потому что его задача — переносить claims о факте аутентификации.
ID Token нужен клиентскому приложению, чтобы понять «кто пользователь» и как он прошёл вход. Его аудитория — client.
Access Token нужен, чтобы обратиться к API. Его аудитория — ресурсный сервер (API).
Правило: не используйте ID Token для вызовов API и не принимайте его на бэкенде как замену access token.
Scopes в OAuth описывают, какие действия разрешены (например, read:orders). Claims в JWT — какой контекст передан (например, sub, aud, iss, иногда роли).
Частая ошибка — пытаться «кодировать права» только через claims, игнорируя scopes и серверную проверку разрешений.
Если вы используете OAuth 2.0/OIDC, почти всегда выгоднее опираться на готовую библиотеку и/или провайдера: они уже корректно реализуют проверку iss/aud/exp, обработку ключей (JWKS), ротацию и особенности потоков.
Самописная реализация обычно ломается на мелочах — и именно эти мелочи становятся уязвимостями.
Большинство проблем возникает не из-за формата JWT, а из-за неверной валидации и операционных решений.
Классическая уязвимость: злоумышленник меняет заголовок токена, подставляя "alg": "none" (или другой неожиданный алгоритм), и рассчитывает, что сервер перестанет проверять подпись.
Защита: сервер должен жёстко ограничивать список допустимых алгоритмов (allowlist) и выбирать алгоритм из конфигурации, а не доверять значению из header.
Если секрет короткий или предсказуемый, его могут подобрать перебором — и тогда можно выпускать «валидные» токены с любыми правами.
Что делать: используйте длинные случайные секреты (например, 32+ байта), храните их в менеджере секретов, регулярно ротируйте и разделяйте ключи между окружениями.
Чем больше данных в токене, тем выше риск утечки и накладные расходы. Проблемы обычно проявляются в:
Храните в токене минимум: идентификатор пользователя, сроки (exp) и нужные технические поля.
aud и issЕсли не проверять iss и aud, API может принять «чужой» токен — например, выпущенный другим сервисом или для другого приложения.
Правило: всегда валидируйте iss, aud, сроки (exp, опционально nbf) и корректность подписи.
Если access‑токен живёт неделю, а его украли — злоумышленник получает неделю доступа.
Практика: делайте короткоживущий access token и отдельный refresh token с возможностью отзыва (хранилище активных refresh‑токенов, ротация, привязка к устройству).
JWT удобен, но безопасность здесь — это не «включили библиотеку и забыли». Ниже — набор шагов, который помогает избежать типовых ошибок при внедрении.
exp (короткий срок для access‑токена).iss и aud.nbf/iat аккуратно, учитывая небольшой сдвиг времени.SameSite и CSRF‑защиту.localStorage проще, но повышает последствия XSS. Если выбираете этот путь — усиливайте Content Security Policy и аудит фронтенда.Держите payload минимальным: идентификатор пользователя, технические поля (iss/aud/exp) и, при необходимости, компактные права (scope/роль). Не кладите туда персональные данные и секреты: payload легко читается.
Не копируйте токены в публичные чаты, тикеты и скриншоты. Для диагностики делайте «красные» логи: маскируйте середину токена, оставляя первые/последние символы, и используйте стенд с тестовыми ключами.
Логируйте события: выпуск токена, обновление, отказ валидации (истёк, неверная подпись, не тот aud/iss), подозрительную частоту запросов, смену ключей.
Если вы делаете продукт «с нуля» и вам важно быстро собрать рабочий прототип с нормальной архитектурой (React на фронте, Go + PostgreSQL на бэкенде, а при необходимости — Flutter для мобильного клиента), удобно, когда каркас приложения и типовые модули появляются за часы, а не недели.
TakProsto.AI как раз про это: вы описываете требования в чате, платформа собирает приложение и помогает итеративно довести авторизацию до продакшн‑уровня — с учётом сроков жизни токенов, проверок iss/aud, refresh‑механики и сценариев отката через snapshots/rollback. Плюс для российского рынка критично, что платформа работает на серверах в России и использует локализованные/opensource LLM‑модели без отправки данных за рубеж.
Если вы выбираете тариф/уровень API‑доступа и хотите включить расширенные лимиты/журналы безопасности, можно добавить внутреннюю справку на странице /pricing.
JWT (JSON Web Token) — это компактный токен в виде строки, который сервер выдаёт после успешного входа. Клиент затем прикладывает его к каждому запросу (часто в Authorization: Bearer <token>), чтобы сервер понял, кто делает запрос и что ему можно.
JWT удобен тем, что сервер обычно проверяет подпись и сроки токена без хранения «сессии» в классическом виде.
Чаще всего после успешной аутентификации (пароль, код, соцсети и т. п.) сервер выдаёт JWT. Затем клиент отправляет его в каждом запросе к API.
Обычно схема такая:
Аутентификация отвечает на вопрос «кто вы?» (доказательство личности). Авторизация отвечает на вопрос «что вам можно?» (права доступа).
JWT обычно переносит результат входа дальше: в токене могут быть sub (кто вы) и роли/права (что можно), чтобы API мог принимать решение при каждом запросе.
JWT обычно состоит из трёх частей: header.payload.signature.
Минимально полезные и часто обязательные поля:
exp — когда токен истекаетsub — стабильный идентификатор пользователяiss — кто выпустил токенaud — для какого сервиса/API токен предназначенДополнительно часто встречаются , , , . Не кладите в секреты и лишние персональные данные — его легко декодировать.
Сервер не должен доверять данным из payload, пока не выполнит строгую валидацию:
Сессии обычно хранят состояние на сервере (например, запись в Redis/БД), а в cookie лежит только идентификатор сессии. Это облегчает отзыв: удалили запись — доступ пропал.
JWT чаще «самодостаточен»: сервер проверяет подпись и сроки без хранилища сессий. Это удобно для API/микросервисов, но отзыв сложнее: токен будет валиден до exp, если не добавлять механизм отзыва.
Коротко по популярным вариантам:
SameSite, CSRF-токен, Secure).Access token нужен для вызовов API и обычно живёт недолго (часто минуты). Refresh token нужен, чтобы получать новый access token без повторного логина и живёт дольше.
Практичные правила:
Частые ошибки, которые реально приводят к уязвимостям:
alg из заголовка и не ограничивать алгоритмы (allowlist)header — тип токена и алгоритм подписи (alg)payload — claims (например, sub, exp, роли)signature — подпись, которая защищает header и payload от незаметной подменыВажно: Base64URL — это не шифрование. payload можно прочитать, даже если подпись валидна.
iatnbfscopejtipayloadexpnbf/iatiss и aud, чтобы не принять «чужой» или «не для этого API» токенПри ошибке обычно возвращают 401 Unauthorized, а в логи пишут причину, но не сам токен целиком.
Частый компромисс для SPA: access-токен в памяти, refresh — в более защищённом канале (например, cookie).
issaudexp без механизма отзываЕсли используете OAuth 2.0/OIDC, проверьте базу: /blog/oauth2-oidc-osnovy — и по возможности опирайтесь на проверенные библиотеки валидации и работы с ключами.