Оглавление
- Введение
- Что такое JWT? RFC 7519 и назначение токенов
- Сессионный подход vs токен на основе JWT
- Структура JWT: Header, Payload, Signature
- Типы JWT: JWS и JWE – подпись vs шифрование
- Примеры использования JWT
- Сценарии использования JWT: аутентификация, авторизация, атрибуты пользователя
- Реализация JWT в Java (Spring Boot) и Go (Gin)
- Безопасность JWT: лучшие практики и уязвимости
- Заключение
- Полезные ссылки и источники
Введение
В современном веб-разработке вопрос безопасной и масштабируемой аутентификации и авторизации пользователей стал как никогда актуален. Классические механизмы сессий на стороне сервера постепенно уступают место более гибким подходам, основанным на токенах, среди которых наибольшее распространение получил JWT (JSON Web Token).
JWT – это компактный, самодостаточный и криптографически подписанный токен, позволяющий передавать информацию между участниками системы без необходимости хранить сессионные данные на сервере. Благодаря своей структуре и универсальности, JWT активно применяется в:
- REST API,
- микросервисных архитектурах,
- мобильных и SPA-приложениях,
- а также во многих реализациях OAuth 2.0 и OpenID Connect.
Однако популярность JWT также породила массу недопониманий: как правильно его генерировать? Как надёжно валидировать? Чем отличается свой токен от токена внешнего провайдера? Какие уязвимости типичны и как их избежать?
Цель данной статьи – не просто объяснить, что такое JWT, но и показать, как он работает на практике, с примерами на Spring Boot (Java) и Go (Gin Framework).
Статья будет полезна как начинающим разработчикам, так и инженерам, переходящим к построению распределенных, безопасных систем.
В ходе материала мы:
- разберём, как устроен JWT изнутри (Header, Payload, Signature),
- покажем отличие JWS от JWE,
- сравним сессионную и токен-ориентированную модели,
- рассмотрим два ключевых сценария: валидация “своего” токена и валидация токена от внешнего OIDC-провайдера,
- внедрим JWT в Spring Boot и Go-приложение,
- проанализируем уязвимости, связанные с JWT, и способы их устранения,
- дополним всё диаграммами PlantUML для лучшего понимания процессов.
Что такое JWT? RFC 7519 и назначение токенов
JSON Web Token (JWT) – это стандарт (RFC 7519), описывающий компактный и URL-безопасный способ передачи определенных утверждений (claims) между сторонами в виде JSON-объекта. Проще говоря, JWT представляет собой цифровой токен, который можно подписывать (или шифровать) и использовать для безопасной передачи информации. Этот токен самодостаточен: он содержит всю необходимую информацию для проверки своей подлинности без обращения к внешнему хранилищу данных. Благодаря цифровой подписи данные внутри JWT могут быть проверены на целостность и источник – получатель токена может убедиться, что он выдан доверенной системой и не был подделан.
JWT-токены широко применяются в современных веб-приложениях, особенно для задач аутентификации и авторизации. Например, после успешного логина пользователь может получить JWT, который затем отправляется с каждым запросом вместо традиционной серверной сессии. В отличие от механизмов с сохранением сессий на сервере, JWT позволяют системе работать без сохранения состояния на стороне сервера (stateless), что упрощает масштабирование и взаимодействие между разными сервисами. JWT стали популярны в экосистемах OAuth 2.0 и OpenID Connect, превращая простые OAuth-серверы авторизации в полноценные identity-провайдеры. В этих стандартах JWT используется как формат ID Token и Access Token, переносящий информацию о пользователе и правах доступа.
Важно понимать, что термин «JWT» обычно относится к токену в целом, однако на практике токен может быть подписан или зашифрован. JWT является частью семейства спецификаций JOSE (JSON Object Signing and Encryption). Если токен подписан, мы имеем дело с JWS (JSON Web Signature); если токен зашифрован, это JWE (JSON Web Encryption). Далее мы подробно рассмотрим структуру JWT, типы токенов и их использование на практике.
Сессионный подход vs токен на основе JWT
В веб-приложениях распространены два основных подхода хранения состояния аутентификации пользователя между запросами: на основе сессий (cookie-based) и на основе токенов (token-based). Оба решают одну задачу – позволяють пользователю не вводить логин и пароль при каждом запросе – но делают это по-разному, со своими плюсами и минусами.
Сессионная аутентификация (на cookies): при классическом подходе после успешного входа на сервере создается сессия, привязанная к пользователю. Сервер хранит данные сессии (например, ID пользователя, время входа, роли и др.) в памяти или базе данных, а клиент получает сеансовый идентификатор в виде cookie. Браузер автоматически отправляет этот cookie с каждым запросом, и сервер по идентификатору находит данные сессии и понимает, что запрос от аутентифицированного пользователя. Такой подход прост и удобен – браузер сам управляет cookie. При выходе пользователя сервер инвалидирует (удаляет) сессию и/или отдает cookie с истекшим сроком, разрывая сессию. Однако сессии требуют хранения состояния: данные о каждом залогиненном пользователе находятся на сервере. Это накладывает ограничения на масштабирование – если у вас несколько серверов, нужно либо “прилипание” пользователя к одному серверу, либо общая база/кэш для сессий, что усложняет архитектуру и может стать узким местом. Кроме того, каждая сессия занимает память, и слишком большое число активных сессий может влиять на ресурсы сервера.
Токен-ориентированная аутентификация (JWT): при подходе с JWT состояние сессии хранится на клиенте в самом токене, а сервер не сохраняет пользовательские данные между запросами. После успешной проверки логина сервер выдает подписанный JWT-токен и отправляет его клиенту (например, в теле ответа или устанавливая в cookie). Браузер или клиентское приложение сохраняет этот токен (в памяти, LocalStorage, или в cookie) и добавляет его в заголовок Authorization: Bearer … при последующих запросах к серверу. Сервер, получив запрос, проверяет подпись токена и извлекает из него информацию о пользователе, правах и т.д., без необходимости обращаться к базе данных. Такой механизм является stateless – сервер ничего не хранит о сессии, он только выдает и проверяет токены. Это значительно упрощает горизонтальное масштабирование и интеграцию микросервисов: любой сервер с нужным секретным ключом может проверить JWT, независимо от других узлов. Также JWT легко использовать в сценариях, когда запросы идут не из браузера, а, например, от мобильных приложений или между сервисами, – не нужно полагаться на браузерные cookies иorigin, токен можно передать в API-запросе.
Сравнение подходов: Использование JWT вместо сессий имеет ряд преимуществ: приложение становится независимым от сервера сессий, легче масштабируется и может работать кросс-доменно (токен можно передавать между доменами/API). JWT – самодостаточный носитель информации о пользователе (идентификатор, роли и пр.), поэтому снижается количество запросов к базе данных за эти сведения при каждом обращении. Однако есть и обратная сторона: безопасность токенов целиком лежит на правильной реализации. В случае серверной сессии разработчик может просто удалить сессию на сервере, чтобы разлогинить пользователя, – и cookie тут же станет невалидным. С JWT же нет встроенного механизма отзыва токена: пока указанное в нем время жизни не истечет, токен считается действительным при проверке подписи. Если пользователя нужно принудительно разлогинить (отозвать токен до экспирации), придется реализовывать дополнительный механизм – например, хранить «черный список» отозванных токенов на сервере и проверять по нему каждую просьбу. Это частично возвращает нас к хранению состояния на сервере, нивелируя преимущество stateless-подхода. Кроме того, компрометация токена несет больший риск: если злоумышленник похитит JWT (например, через XSS-атаку, см. раздел «Безопасность»), он получит доступ к ресурсам от имени пользователя до истечения срока токена. В случае же кражи cookie с session-id можно хотя бы оперативно уничтожить серверную сессию.
В целом, выбор между сессией и JWT зависит от требований проекта. Для традиционных веб-сайтов с серверными рендерингом и одной точкой входа зачастую проще и безопаснее использовать серверные сессии (cookie), получая автоматическую защиту от повторного использования токена при логауте и встроенную отправку cookie браузером. Но в распределенных системах и REST API (особенно при работе с мобильными или SPA-клиентами) JWT-токены дают большую гибкость и масштабируемость. Нередко эти подходы комбинируют: например, хранят JWT в http-only cookie, совмещая преимущества (статус на клиенте + автоматическая отправка браузером). Важно лишь тщательно проработать безопасность, о чем мы подробно поговорим ниже.
Структура JWT: Header, Payload, Signature
JWT-токен имеет строго определенную структуру – это строка, состоящая из трех частей, разделенных точками: Header.Payload.Signature
. Каждая часть – это данные в формате JSON, закодированные в Base64 URL-safe.
Структура JWT. На схеме слева показаны три основные части токена, а справа – пример содержимого. Header обычно содержит тип токена (JWT
) и алгоритм подписи (HS256
и др.). Payload включает набор claims – утверждений о пользователе и параметров токена (например, идентификатор пользователя, роли, время выпуска и истечения). Signature – это криптографическая подпись, полученная путем вычисления HMAC-SHA256 от соединения закодированного заголовка и payload с секретным ключом (при симметричном алгоритме). Подпись служит для проверки целостности и подлинности: она гарантирует, что никто не изменил данные токена с момента его выпуска.
Рассмотрим каждую часть подробнее:
- Header (заголовок): содержит метаданные токена. Обязательные поля – это тип токена
typ
(для JWT всегда"JWT"
) и алгоритм подписиalg
(например,"HS256"
для HMAC-SHA256,"RS256"
для RSA и т.д.). Могут присутствовать дополнительные поля, напримерkid
(key ID) – идентификатор ключа, которым подписан токен, используемый для выбора ключа из набора (актуально при асимметричной подписи, об этом далее).
{
"alg": "HS256",
"typ": "JWT"
}
- Payload (полезная нагрузка): содержит сами claims – утверждения о субъекте токена (пользователе или системе) и другие данные. Существует ряд стандартных (зарезервированных) полей: например,
iss
(issuer, издатель токена),sub
(subject, субъект токена – обычно идентификатор пользователя),aud
(audience, аудитория токена – для какого приложения или сервиса он предназначен),exp
(expiration time, время истечения),iat
(issued at, время выпуска),nbf
(not before, время, раньше которого токен недействителен),jti
(JWT ID, уникальный идентификатор токена). Эти поля необязательны, но часто используются. Помимо них, payload может содержать любые публичные или приватные claims – произвольные пары «ключ-значение» с дополнительной информацией. Например, можно включитьname
илиemail
пользователя, список его ролей (roles
) или разрешений. Важно помнить, что содержимое payload не шифруется (в случае JWS-токена) – оно только закодировано в Base64. Любой, кто получит JWT, может расшифровать payload и прочитать данные, поэтому туда нельзя помещать конфиденциальную информацию в открытом виде (пароли, личные данные, секретные ключи и т.п.).
Пример payload с некоторыми стандартными claim и дополнительными полями:
{
"iss": "myapp.example.com",
"sub": "user12345",
"iat": 1644744000,
"exp": 1644768000,
"role": "admin",
"name": "John Doe"
}
Здесь iss
указывает, кем выдан токен (наш сервис), sub
– идентификатор пользователя, iat
/exp
– метки времени выпуска и истечения токена (в формате UNIX timestamp). Дополнительно добавлены role
и name
пользователя.
- Signature (подпись): результат криптографической функции, обеспечивающий защиту токена. Для формирования подписи библиотека объединяет закодированный Header и Payload через точку, и вычисляет на них цифровую подпись с помощью указанного в Header алгоритма и секретного ключа. Например, при алгоритме HS256 формула подписи такая:
signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret_key )
Для RSA- или ECDSA-алгоритмов вместо общего секрета используется приватный ключ для подписи и публичный ключ для проверки. Подпись позволяет проверить, что Header и Payload не были изменены: при валидации JWT сервер заново вычисляет подпись из пришедших Header и Payload и сравнивает с переданной. Если они не совпадают – токен был подделан или поврежден и отклоняется.
Итоговая сериализация JWT выглядит как одна строка: три блока Base64, разделенных точками, например:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvbiBEb2UiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE2NDQ3NjgwMDB9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
Этот токен можно легко декодировать, используя инструменты вроде jwt.io или командных утилит, чтобы увидеть содержимое Header и Payload. Подпись же не расшифровывается, а проверяется путем вычисления заново.
Типы JWT: JWS и JWE – подпись vs шифрование
По сути, JWT – это контейнер для данных, который можно либо подписать (для защиты целостности), либо зашифровать (для конфиденциальности), либо и то, и другое сразу. В спецификации различаются два варианта:
- JWS (JSON Web Signature): подписанный JWT. Это самый распространенный тип токенов. В JWS-токене Payload остается открытым (незашифрованным) и может быть прочитан любым получателем, но защищен цифровой подписью. Подпись гарантирует, что данные подлинные и не модифицированы: получатель, зная секрет (для HMAC) или публичный ключ (для RSA/ECDSA), может убедиться, что токен действительно выдан доверенной стороной. JWT в веб-аутентификации почти всегда представляют собой JWS. Такие токены удобны тем, что их можно свободно пересылать и кешировать – никаких дополнительных действий, кроме проверки подписи, не нужно. Однако данные в них не являются приватными: хранить чувствительную информацию нельзя, т.к. любой может декодировать payload.
- JWE (JSON Web Encryption): зашифрованный JWT. В этом случае содержимое Payload (и опционально Header) шифруется получателю. JWE обеспечивает конфиденциальность – даже перехватив токен, злоумышленник не прочитает его содержимое без ключа расшифровки. Однако сам по себе JWE не гарантирует подлинности: если токен только зашифрован, но не подписан, получатель не может быть уверен, кем он создан (если злоумышленник получил публичный ключ шифрования, он тоже мог создать токен, хоть и не читая содержимое). Поэтому обычно JWE-токены применяют в сочетании с подписью: например, сначала подписать данные (JWS), потом результирующую строку зашифровать – так достигаются и целостность, и конфиденциальность. Структура JWE сложнее: помимо зашифрованного Payload, включает зашифрованный ключ сессии, заголовок с алгоритмами, IV, тег аутентификации и др.. Все эти части соединяются точками, аналогично JWS.
Когда использовать JWE? Зашифрованные JWT встречаются значительно реже. В большинстве случаев достаточно подписать токен и просто не помещать в него секретные данные. Сценарии для JWE – когда нужно передать через сторонние системы зашифрованную информацию (например, медицинские данные или персональные сведения) и вы хотите использовать JWT как оболочку. Тогда отправитель шифрует токен на публичном ключе получателя (чтобы только он смог прочесть). Если же данные не столь критичны или уже доступны получателю, лишнее шифрование только увеличит размер токена и нагрузку на систему. Поэтому для задач аутентификации и обмена авторизационной информацией практически всегда используются JWT с подписью (JWS).
Для справки: спецификации JWS и JWE определены в отдельных RFC (7515 и 7516 соответственно). Но разработчикам зачастую достаточно понимать, что «обычный» JWT – это подписанный токен. Если же возникает потребность скрыть содержимое JWT, можно рассмотреть JWE или другой канал шифрования.
Примеры использования JWT
Рассмотрим, как JWT применяется на практике в разных сценариях. Типичные случаи – это когда приложение само выпускает JWT для своих пользователей, а также когда приложение принимает JWT от стороннего сервиса (например, стороннего провайдера авторизации). В обоих ситуациях важна правильная валидация токена.
Валидация JWT, выпущенного приложением
Когда токен генерируется и используется в пределах одного приложения (или набора своих сервисов), задача достаточно проста. После того как пользователь прошел аутентификацию (например, отправил логин и пароль, или выполнил OAuth-авторизацию), сервер создает JWT и возвращает его клиенту. Клиент сохраняет токен и отправляет с каждым запросом. Серверу для валидации нужно знать секретный ключ (если используется HMAC) или публичный ключ (если токен подписан асимметрично). Так как токен наш собственный, эти ключи известны приложению изначально.
Алгоритм проверки такой JWT на сервере включает несколько шагов:
- Извлечь токен из запроса – обычно он передается в HTTP-заголовке
Authorization: Bearer <token>
(либо в cookie, если решено хранить JWT в cookie). Например, в Go с Gin можно получить заголовок и отделить префикс “Bearer”:tokenString := c.GetHeader("Authorization")
(в реальном коде нужно убрать префикс и проверить, что заголовок есть). В Spring Boot при использовании Spring Security токен автоматически вынимается фильтром из заголовка. - Распарсить токен и проверить подпись. С помощью JWT-библиотеки выполняется разбор строки токена: вычисляется подпись заново и сравнивается с ожидаемой. В псевдокоде на Java это выглядит так:
Jwts.parserBuilder()
.setSigningKey(secretKey) // секрет для HS256 или публичный ключ для RS256
.build()
.parseClaimsJws(tokenString) // парсит и одновременно проверяет подпись
.getBody();
Если ключ верный и токен не подделан, парсер вернет объект Claims (payload). При любом несоответствии подписи или формата будет выброшено исключение (токен недействителен). Аналогичный код на Go при помощи библиотеки golang-jwt/jwt (v5):
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(secretKey), nil // секретный ключ для проверки подписи
})
if err != nil || !token.Valid {
// подпись не сошлась или токен просрочен/битый – отклоняем
}
claims := token.Claims.(jwt.MapClaims)
// далее можно получить поля, например:
userID := claims["sub"]
roles := claims["roles"]
Здесь jwt.Parse
самостоятельно проверяет подпись токена с использованием переданного ключа. Если нужен контроль алгоритма, можно дополнительно проверить token.Header["alg"]
. В нашем примере мы ожидаем, что токен подписан HS256 и знаем секрет.
- Проверить стандартные claims: часто токен содержит поле
exp
(время истечения) – сервер должен убедиться, что текущее время меньше указанного и токен не просрочен. Также можно проверитьnbf
(не раньше определенного времени),iss
(совпадает ли издатель с нашим приложением),aud
(предназначен ли токен для этого сервиса). Например, получив Claims через.getBody()
илиtoken.Claims
, можно сделать:
Date expires = claims.getExpiration();
if (expires.before(new Date())) { /* токен просрочен, отвергнуть */ }
String issuer = claims.getIssuer();
if (!"myapp.example.com".equals(issuer)) { /* чужой токен, отвергнуть */ }
Эти проверки зависят от того, какие поля вы записываете при генерации токена.
- Определить пользователя из токена. После успешной криптографической проверки можно считать токен аутентифицированным. Из claims извлекаются идентификатор или имя пользователя (например, поле
sub
или другой custom claim). Далее приложение обычно загружает информацию о пользователе (например, из базы) или прямо использует claims токена (например, роли) для авторизации. - Решить, имеет ли пользователь доступ к запрошенному ресурсу (авторизация). JWT может содержать, например, claim
role: admin
или список прав. На основе этих данных и логики приложения происходит проверка, можно ли выполнять данный запрос. (Подробнее о сценариях авторизации – в следующем разделе.)
При собственных JWT нет необходимости в внешних сетевых запросах при валидации – весь механизм работает локально с помощью криптографических функций. Важно лишь хранить секретный ключ в конфигурации безопасно (не светить его в коде или в репозитории). Например, в Spring Boot секрет можно положить в application.properties
, а доступ к нему дать через аннотацию @Value
. Никогда не храняйте ключ прямо в коде или в публичных местах – обладая вашим HMAC-секретом, злоумышленник мог бы подписывать любые токены и выдавать себя за любого пользователя.

Рис 1. Пример системы, работающей с внешним провайдером.
Для генерации JWT в приложении используются библиотеки. В Java популярен пакет jjwt (io.jsonwebtoken), в котором создание токена выглядит так:
String token = Jwts.builder()
.setSubject(user.getUsername())
.claim("role", user.getRole())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600_000)) // 1 час
.signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
.compact();
Аналогично, в Go (с использованием библиотеки github.com/golang-jwt/jwt) создание токена:
claims := jwt.MapClaims{
"sub": username,
"role": "ADMIN",
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(secretKey))
Эти фрагменты кода иллюстрируют, как в токен помещаются необходимые данные (subject
, роли, сроки) и как он подписывается секретным ключом. Полученный tokenString
отправляется клиенту (например, в теле JSON-ответа или в заголовке).
На стороне сервера проверку и разбор JWT обычно выносят в отдельный фильтр / middleware. В случае Spring Security создается фильтр (унаследованный от OncePerRequestFilter
), который при каждом запросе делает примерно следующее: ищет JWT в заголовке Authorization, если находит – валидирует его (через сервис, как описано выше), затем на основе данных токена создает объект аутентификации (например, UsernamePasswordAuthenticationToken
) и помещает его в SecurityContext
Spring. Это позволяет остальной части приложения (контроллерам, сервисам) узнавать, какой пользователь выполняет запрос, и применять @PreAuthorize и другие механизмы авторизации. Если же токена нет или он неверен – фильтр не аутентифицирует запрос, и дальше по цепочке Spring Security может вернуть 401 Unauthorized автоматически. В Go с Gin middleware для JWT делает аналогичную вещь: при каждом запросе на защищенный маршрут middleware вытаскивает токен, проверяет его как выше, и либо устанавливает в контекст информацию о юзере (c.Set("user", userID)
) и вызывает c.Next()
для продолжения обработки, либо прерывает запрос (c.AbortWithStatus(401)
) если токен недействителен. Разработчик регистрирует этот middleware для нужных маршрутов, и тем самым защищает их JWT-аутентификацией.
Валидация JWT от внешнего провайдера (OpenID Connect, OAuth2)
Другой распространенный случай – ваше приложение не само выдает токены, а доверяет стороннему провайдеру аутентификации. Это может быть OAuth2/OpenID Connect сервер (например, Google, Facebook, Auth0, Keycloak и т.д.), который после логина пользователя перенаправляет в ваше приложение JWT – обычно это ID Token (JWT с информацией о пользователе) или Access Token для API. Как приложению убедиться в подлинности такого токена?
Здесь нельзя использовать заранее известный секрет, ведь токен подписан чужим сервисом. Однако JWT протокол рассчитан на этот случай посредством механизма JSON Web Key Set (JWKS). Провайдер, выпускающий токены, публикует набор своих открытых ключей в формате JWKS – обычно по стандартному URL (например: https://login.example.com/.well-known/jwks.json
). В JWT токене (в Header) при этом указывается ключ, которым он подписан – через поле kid
(Key ID), а также алгоритм (alg
, например RS256).
JWKS (JSON Web Key Set) – это JSON-документ, содержащий массив публичных ключей провайдера. Каждый ключ снабжен параметрами: kid
(идентификатор), kty
(тип, например RSA), модуль n
и экспонента e
для RSA, использование use
(sig для подписей) и пр.. Ваше приложение может загрузить JWKS провайдера (это делается либо при запуске, либо при первом получении токена, либо библиотеками автоматически) и найти в этом наборе ключ с нужным kid
. Получив параметры ключа (например, модуль и экспонента RSA), вы создаете из них публичный ключ. Далее процесс верификации такой же, как и раньше: библиотека JWT проверяет подпись токена, но теперь вместо вашего секрета использует публичный ключ поставщика.

Рис.2 : выше приведена иллюстрация процесса проверки JWT с использованием JWKS: приложение запрашивает у IdP набор ключей, находит нужный публичный ключ по kid, и валидирует подпись токена.
Таким образом, вализация стороннего JWT сводится к двум вещам: получению правильного ключа и проверке claims. Последнее особенно важно: чужой токен может быть подписан правильно, но предназначен не для вашего приложения. Поэтому обязательно проверяйте поля iss
(совпадает ли с доверенным identity-провайдером) и aud
(содержит идентификатор вашего приложения или ожидаемое значение). OpenID Connect ID Token, например, содержит aud
равный client_id вашего приложения, и iss
равный URL провайдера – эти поля следует сверять с конфигом. Также стоит обращать внимание на exp
/iat
, чтобы токен не был устаревшим или из будущего.
Современные фреймворки могут облегчить эту работу. В Spring Boot есть starter для Spring Security OAuth2 Resource Server, куда достаточно прописать URL Issuer-а (Issuer URI), и он сам скачает JWKS, выберет ключ и будет валидировать токены (библиотека Nimbus JOSE JWT под капотом). В Go существуют библиотеки, например golang.org/x/oauth2/jwt или пакеты от Auth0, которые умеют загружать JWKS и кэшировать ключи. Но даже вручную этот процесс реализуем: JWKS обычно меняется нечасто (ключи провайдера ротоируются раз в несколько дней/недель), поэтому приложение может периодически обновлять их копию.
JSON Web Key (JWK) – это отдельный ключ в формате JSON. Он может представлять публичный или симметричный ключ. В случае RSA содержит модуль n
и экспонент e
в base64url. JWKS – это просто JSON с массивом ключей (под полем "keys"
). Ниже пример фрагмента JWKS (сокращен):
{
"keys": [
{
"kid": "42148Kf",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "iGaLqP6y-SJCCBq5Hv6pGDbG_SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jX...",
"e": "AQAB"
},
{ ... другой ключ ...}
]
}
По kid
заголовка нашего JWT мы, например, выберем ключ с "kid": "42148Kf"
. Используя n
и e
, можно воссоздать RSAPublicKey
. Дальше – стандартная проверка подписи. Токены часто включают kid
именно для того, чтобы потребителю не пришлось перебирать все ключи – достаточно сразу выбрать нужный.
Стоит отметить, что при интеграции с внешними OpenID Connect-провайдерами помимо проверки подписи, обычно происходит и получение профильной информации. Например, после валидного ID Token вы можете вызвать UserInfo endpoint провайдера или использовать данные из токена (имя, email) напрямую. Это за рамками обсуждения JWT, но важно понимать контекст: JWT облегчает доверие между сервисами (вашим приложением и identity-провайдером) за счет криптографии.
Итак, резюмируя проверку чужого JWT:
- Получаем/кешируем JWKS провайдера, выбираем нужный публичный ключ.
- Проверяем подпись токена этим ключом.
- Сверяем claims токена (issuer, audience, exp и др.) с ожидаемыми значениями.
- Считаем токен аутентифицированным и извлекаем из него необходимые данные (например, идентификатор пользователя, email, роли).
Сценарии использования JWT: аутентификация, авторизация, атрибуты пользователя
JWT применяется в нескольких связанных, но разных сценариях:
- Аутентификация (authentication): подтверждение личности пользователя. В этом сценарии JWT выступает аналогом «удостоверения личности». Например, при логине пользователь вводит пароль, сервер проверяет его и возвращает JWT, который содержит информацию о пользователе (как минимум его идентификатор,
sub
). Наличие у клиента корректно подписанного JWT означает для сервера, что пользователь уже прошел аутентификацию ранее. Таким образом, JWT позволяет реализовать многоразовый логин: пользователь вводит пароль один раз, получает токен, и затем предъявляет токен при каждом запросе, чтобы подтвердить, кто он. В протоколах OAuth2/OIDC JWT используется в роли ID Token – токена, который подтверждает, что «пользователь X аутентифицирован провайдером Y». - Авторизация (authorization): определение прав доступа. JWT удобно не только идентифицировать пользователя, но и передавать его роли, права или другие атрибуты, нужные для принятия решений на сервере. Например, в токен можно включить claim
role: admin
илиpermissions: ["read","write"]
. При поступлении запроса сервер читает эти claims и решает, разрешено ли пользователю запрашиваемое действие. Это особенно полезно в микросервисной архитектуре: вместо того чтобы каждый сервис запрашивал центральный сервер авторизации «а какие права у пользователя X?», каждый сервис может доверять информации, вложенной в JWT (раз он подписан доверенным источником). Такой подход называется явным распространением контекста безопасности. Примеры: токен доступа в OAuth содержит scope – список дозволенных операций; JWT внутри организации может содержать отдел сотрудника, грейд доступа и т.п. - Передача пользовательских атрибутов между компонентами: JWT часто используют для обмена информацией о пользователе между разными сервисами. Например, фронтенд-приложение после логина получает JWT с некоторыми данными пользователя (имя, email, настройки) и может передавать этот токен при запросах к другим сервисам – те получат необходимую базовую информацию из токена, не делая дополнительных запросов к первоисточнику. В распределенных системах JWT стал стандартом передачи сведений об идентификации и правах, поскольку каждый сервис, получив токен, автономно знает, кто пользователь и что ему разрешено. Это снижает связанность и нагрузку на центральную БД или авторизационный сервис. Кроме того, JWT применяется для SSO (Single Sign-On): одна система аутентифицирует пользователя и выпускает JWT, а другие системы, получив этот JWT, «доверяют» ему и автоматически логинят пользователя у себя (конечно, после проверки подписи и аудитории токена). Многие крупные платформы реализуют SSO именно на JWT/OpenID Connect.
Пример использования JWT в потоках аутентификации и авторизации. 1) Пользователь вводит свои учетные данные (или использует вход через Google/Facebook). 2) Сервер проверяет их и создает JWT (подписывает токен). 3) Токен отправляется клиенту, который сохраняет его (например, в память приложения или httpOnly cookie). 4) При последующих запросах клиент отправляет токен в заголовке Authorization
. 5) Сервер принимает токен и валидирует его (проверяет подпись, срок и пр.). 6) Запрос на защищенный ресурс (например, GET /user/info
) выполняется, сервер на основе информации токена определяет права доступа. 7) Если токен валидный и у пользователя есть права – сервер возвращает запрошенные данные. 8) Если токен недействителен или просрочен – возвращается ошибка (например, 401 Unauthorized
). Этот сценарий показывает, как JWT позволяет единожды залогинившись, обращаться к разным ресурсам без повторной аутентификации, и как на каждом шаге проверяется авторизация по данным токена.
В реальных приложениях JWT может использоваться сразу во всех трех аспектах: как удостоверение личности (аутентификация), как хранилище ролей/прав (авторизация), и как контейнер для переноса дополнительной информации о пользователе. Например, токен может содержать sub: user123
, role: admin
и даже некие пользовательские настройки или признаки – все зависит от задач. Но тут важно помнить про баланс между полезностью и безопасностью: включать в токен следует минимально необходимый набор сведений (принцип наименьших привилегий и раскрытия информации).
Хорошей практикой является разделение токенов по назначению. Например, в OAuth2 есть понятия Access Token и ID Token: первый используется только для доступа к ресурсам (и в нем хранятся scopes/права, он может быть JWT или нет), а второй – для передачи информации о пользователе клиентскому приложению (всегда JWT в OIDC). Подобно этому, вы можете выпустить один JWT для внутренних сервисов с подробными правами, а другой – для фронтенда с только минимальными данными для интерфейса.
Реализация JWT в Java (Spring Boot) и Go (Gin)
Теперь рассмотрим практические аспекты реализации JWT в приложении на примерах Spring Boot (Java) и Go (Gin). Мы посмотрим, как генерировать токены, как их проверять и интегрировать в механизм защиты маршрутов (middleware/filters).
JWT в Spring Boot (Spring Security)
В Spring Boot с Spring Security настройка JWT обычно включает создание точки выдачи токена (например, REST-контроллера /login
, если не используется стандартный OAuth2) и фильтра проверки токена для остальных запросов.
Генерация токена. Предположим, у нас есть сервис AuthController
с методом /login
, принимающим учетные данные. После проверки пользователя (через UserDetailsService, AuthenticationManager и т.д.), мы создаем JWT. Как мы показывали ранее, с библиотекой JJWT (от компании Okta) это делается через Jwts.builder()
. В коде это может выглядеть так:
@RestController
public class AuthController {
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest req) {
// аутентификация пользователя
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword()));
UserDetails user = userDetailsService.loadUserByUsername(req.getUsername());
// генерация JWT
String token = jwtService.generateToken(user);
return ResponseEntity.ok(new JwtResponse(token));
}
}
Здесь jwtService.generateToken(user)
инкапсулирует логику создания токена (мы привели пример реализации JwtService выше). Токен возвращается клиенту, обычно в теле ответа (либо устанавливается в cookie). Стоит отметить: если вы используете Spring Security с формой логина, то можно настроить UsernamePasswordAuthenticationFilter
на выдачу JWT при успешном входе – но в современных приложениях чаще делают как в примере: свой контроллер логина.
Валидация токена (фильтр). Для обработки JWT при запросах создается фильтр, который запускается до основных контроллеров. Пример упрощенного фильтра:
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Autowired
private JwtService jwtService;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
String jwt = null;
String username = null;
// Извлечь токен из заголовка
if (authHeader != null && authHeader.startsWith("Bearer ")) {
jwt = authHeader.substring(7);
username = jwtService.extractUserName(jwt); // извлечь из токена имя пользователя (метод возвращает sub)
}
// Проверка, что пользователь не аутентифицирован в контексте
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// проверяем валидность токена
if (jwtService.isTokenValid(jwt, userDetails)) {
// Создаем Authentication и ставим в контекст
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
В этом фильтре jwtService.extractUserName
и jwtService.isTokenValid
используют нашу JWT-библиотеку для парсинга токена и сравнения username + проверки срока и подписи. Если все хорошо – мы создаем Authentication
объект с данными пользователя и отмечаем запрос как аутентифицированный. Теперь в контроллерах можно использовать @AuthenticationPrincipal
чтобы получить текущего пользователя, или просто доверять, что SecurityContextHolder.getContext().getAuthentication()
содержит нужную информацию. В итоге, весь поток работы с JWT становится прозрачным: разработчик маркирует нужные endpoints аннотацией @PreAuthorize("hasRole('ADMIN')")
или настраивает HttpSecurity
политику, а Spring Security уже будет знать про пользователя из токена.
Конфигурация Spring Security. Не забудьте зарегистрировать созданный фильтр в цепочке Spring. Обычно это делается в классе, наследующем WebSecurityConfigurerAdapter
(для Spring Security 5.x) или через компонент SecurityFilterChain
(для Spring Boot 3 / Security 6). Фильтр JWT следует добавлять перед фильтром UsernamePasswordAuthenticationFilter (если вы используете форму логина) или вообще первым, если нет другого механизма. Также нужно отключить хранение сессии (sessionCreationPolicy.STATELESS
), раз уж мы перешли на JWT.
Пример настройки (SecurityConfig):
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/login", "/public/**").permitAll();
auth.anyRequest().authenticated();
})
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
Здесь jwtAuthFilter
– наш компонент фильтра. Мы отключаем CSRF (т.к. не храним сессию, а используем JWT), открываем анонимный доступ на логин и какие-то публичные ресурсы, остальное – только для аутентифицированных. SessionCreationPolicy.STATELESS говорит Spring не хранить сессию между запросами.
Итого для Spring: мы задействовали стандартные механизмы Spring Security, добавив свой фильтр. Альтернативно, как упоминалось, можно было использовать Spring Boot 3 + Spring Security 6, где поддержка JWT Resource Server встроена: достаточно указать property spring.security.oauth2.resourceserver.jwt.jwk-set-uri=<JWKS_URL>
для внешнего провайдера или spring.security.oauth2.resourceserver.jwt.secret=<HS256_SECRET>
для своего токена – и Spring сам будет валидировать Authorization заголовок. Такой способ особенно хорош для сценария с внешним OpenID Connect (он избавляет от написания собственного фильтра).
JWT в Go (Gin) – реализация middleware
В Go нет встроенного «фреймворка безопасности», поэтому интеграция JWT – это написание пользовательского middleware. Используем популярный веб-фреймворк Gin и библиотеку golang-jwt/jwt (преемник jwt-go).
Генерация токена. Можно реализовать функцию для выдачи JWT, как мы делали в разделе 5. Например, при успешной аутентификации (проверке логина/пароля в базе) мы создаем токен:
import "github.com/golang-jwt/jwt/v5"
// ... внутри обработчика /login:
if username == "demo" && password == "pass" {
claims := jwt.MapClaims{
"sub": username,
"role": "user",
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(secretKey))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Cannot generate token"})
return
}
// можно вернуть токен в JSON или установить cookie
c.JSON(http.StatusOK, gin.H{"token": tokenString})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
}
Здесь мы заполняем стандартные поля – sub, exp, iat, а также кладем role
. Секретный ключ берется из настроек (он должен быть []byte). После этого tokenString
отсылается клиенту.
Middleware для проверки. Теперь напишем функцию-мидлварь:
func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 1. Получаем токен
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"})
return
}
tokenString := authHeader[len("Bearer "):]
// 2. Верифицируем токен
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Проверяем, что алгоритм – HS256, который мы ожидаем
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(secretKey), nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
return
}
// 3. Можно извлечь данные пользователя из токена
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
return
}
username := claims["sub"].(string)
role := claims["role"].(string)
// 4. Сохраняем информацию в контексте, чтобы хендлеры могли ее использовать
c.Set("user", username)
c.Set("role", role)
c.Next() // пропускаем запрос дальше
}
}
Зарегистрируем этот middleware для нужных маршрутов:
router := gin.Default()
protected := router.Group("/api")
protected.Use(JWTAuthMiddleware())
{
protected.GET("/profile", userProfileHandler)
protected.POST("/admin", adminOnlyHandler)
}
Теперь все запросы к /api/profile
и т.д. должны иметь валидный JWT. В обработчиках можно получить c.Get("user")
чтобы знать, кто обращается. Также при необходимости можно прямо внутри мидлвари проверить авторизацию (например, для adminOnlyHandler убедиться, что role == "admin"
, иначе вернуть 403).

Рис 3. Диаграмма последовательности для процесса валидации токена.
В приведенном middleware есть проверка алгоритма на этапе jwt.Parse
– это защита от некоторых атак, когда, например, злоумышленник мог бы подменить алгоритм. Мы ожидаем HS256 и явно это оговариваем. Также, jwt.Parse
сам проверит exp
при наличии (в v5 библиотеки это по умолчанию, если токен просрочен – token.Valid будет false).
Использование JWKS в Go: Если бы нам нужно было доверять внешнему JWT (например, от Auth0), можно было бы вместо прямого secretKey
в функции parse сделать HTTP-запрос к JWKS URL. Однако есть готовые решения – библиотека github.com/coreos/go-oidc
(ныне github.com/coreos/go-oidc/v3/oidc
), которая интегрируется с golang.org/x/oauth2
. Она умеет загружать .well-known/openid-configuration, получать JWKS и валидировать ID-токены. В простых случаях можно вручную: загрузить JSON с ключами (например, с Auth0 /.well-known/jwks.json
), распарсить его в структуру, найти нужный ключ. Затем в jwt.Parse
вместо функции, возвращающей []byte, вернуть rsa.PublicKey (соответственно, token.Method
нужно будет сравнить с RSA методами).
Refresh токены и продление сессии: В Go, как и в любом другом окружении, вы можете реализовать пару “access_token + refresh_token”. Например, выдавать короткоживущий JWT и долгоживущий refresh-токен (можно JWT или просто случайная строка) в httpOnly cookie. При истечении JWT клиент автоматически обращается к /refresh
с cookie, сервер проверяет refresh-токен (по базе или списку) и выдает новый JWT. Эта логика больше связана с общей архитектурой, JWT же в ней продолжает выполнять роль переносимого удостоверения.
Безопасность JWT: лучшие практики и уязвимости
JWT предоставляет удобство, но также привносит свои риски. Рассмотрим ключевые моменты безопасности и как защитить приложение при использовании JWT.
Не храните конфиденциальные данные в Payload. Как уже отмечалось, payload обычного JWS-токена не зашифрован. Он закодирован base64, что декодируется за секунды. Поэтому никогда не помещайте в JWT секреты или личную информацию, раскрытие которой нежелательно. Например, пароли, номера документов, финансовые данные или PII не должны находиться внутри токена в открытом виде. Даже если токен подписан, любой клиент или злоумышленник, заполучивший его, сможет прочитать содержимое. OWASP прямо предупреждает: «для подписанных незашифрованных токенов нельзя хранить чувствительные данные, так как JWT защищен от подделки, но его содержимое может прочитать кто угодно». Payload должен содержать только ту информацию, которую вы не возражаете сделать видимой обладателю токена (обычно это технические идентификаторы, роли и т.п.). Если возникает потребность перенести через токен что-то конфиденциальное, рассмотрите вариант использования JWE (шифрования) либо не включайте эти данные вовсе, а запрашивайте их с сервера по необходимости.
Защита от подделки токена (подпись и алгоритмы). Безопасность JWT напрямую зависит от криптографической подписи, поэтому важно правильно выбирать и хранить ключи. Во-первых, используйте надежный алгоритм. Не применяйте alg: none
– этот псевдо-алгоритм означает отсутствие подписи и был причиной серьезной уязвимости в ранних реализациях JWT-библиотек. Современные библиотеки по умолчанию отключают поддержку none
, но стоит быть внимательным. Также избегайте устаревших алгоритмов вроде HS256, RS256 при слишком коротких ключах. Во-вторых, храните секретные ключи в тайне. Ключ подписи JWT (особенно при HMAC) – это эквивалент мастер-пароля ко всем токенам. Его компрометация равносильна полному взлому системы аутентификации. Не вшивайте его в клиентские скрипты, не коммитьте в репозиторий; храните в безопасном хранилище настроек (Vault, переменные окружения). В-третьих, используйте достаточную длину секрета. Для HMAC-регистра стоит брать случайную строку длиной не менее 32 байт (а лучше 64 байта), с хорошей энтропией. Поскольку этот секрет никогда не придется вводить вручную, его длина никак не ограничена удобством – воспользуйтесь генератором cryptographically secure random. Слабый секрет может быть взломан перебором: атаки с подбором HMAC-ключа реально проводились с помощью утилит вроде Hashcat. Также, если возможно, переходите на асимметричные ключи (RSA/ECDSA): тогда даже при компрометации сервиса-потребителя токенов ваш основной приватный ключ останется на авторизационном сервере (клиенты получают только публичный ключ). Но помните, что приватный ключ RSA тоже должен быть хорошо защищен (например, от утечки через уязвимости на сервере). В-четвертых, при настройке библиотек явно указывайте, какой алгоритм ожидать. Это предотвращает так называемые атаки переключения алгоритма: были случаи, когда злоумышленник менял header токена с RS256
на HS256
, подсовывал вместо подписи свой HMAC, а сервер по незнанию принимал публичный ключ RSA как секрет для HMAC. Такие конфигурационные дыры нужно исключить проверкой alg
и использованием проверенных библиотек.
Безопасное хранение токена на клиенте. Если JWT используется в браузерном приложении (SPA, мобильном), возникает вопрос – где его хранить? Плохая идея – хранить JWT в plain JavaScript переменной или localStorage
, т.к. при XSS-уязвимости скрипт злоумышленника сможет прочитать и похитить токен. Рекомендация OWASP – использовать HttpOnly cookies для токенов, чтобы JavaScript на странице не мог их трогать. Однако cookie уязвимы к CSRF-атакам. Возможны подходы: хранить access-token в памяти (Redux store) и refresh-token в httpOnly cookie; либо использовать SameSite
атрибут для cookie, защищая от CSRF. Решение зависит от приложения, но главное – минимизировать возможность кражи токена. Если все же токен хранится в localStorage, убедитесь, что ваше приложение максимально защищено от XSS (контекстная экранизация, Content Security Policy и пр.), иначе утечка токена приведет к захвату учетной записи пользователя. Многие считают, что JWT не следует хранить дольше текущей сессии (т.е. не делать “Remember me” на месяцы); лучше при каждом новом заходе пользователя требовать снова войти или обновить токен через refresh.
Срок жизни токена и обновление (refresh). Выбор времени жизни (exp
) – критически важный параметр безопасности. Короткий TTL (например, 15 минут или 1 час) ограничивает окно, в котором украденный токен может быть использован. OWASP рекомендует делать JWT короткоживущими и не полагаться только на их непохищаемость. В сочетании с refresh-токенами это дает наилучший баланс: основной JWT действует недолго, а refresh-токен (который, например, хранится только в httpOnly cookie и передается автоматически) может продлевать сессию без повторного ввода пароля. При этом, refresh-токен лучше сделать одноразовым или отзывным: при его использовании выдавать новый, а старый помечать недействительным в базе. Многие стандарты (например, OIDC) так и делают: refresh token можно отозвать, тем самым оборвав сессию пользователя досрочно. Access-token же обычно не отзывается, просто ждут его истечения. Если вы не хотите внедрять хранение на сервере вообще, альтернативный подход – очень короткий срок жизни JWT (несколько минут) без refresh: тогда даже без отзыва риск снижается, но пользователь может замечать более частые повторные логины.
Отзыв JWT (Logout и аннулирование). Как указано ранее, JWT по своей природе не привязан к серверной сессии, поэтому разработчику приходится самому решать, как поступать с “выходом из системы” или компрометацией токена. Основные стратегии:
- Blacklisting (черный список): сервер хранит идентификаторы или хеши токенов, которые больше не доверяет. Например, можно добавить claim
jti
(уникальный ID токена при выпуске) и при логауте сохранять этот jti в Redis как отозванный до моментаexp
токена. При каждом запросе тогда проверять, не числится ли jti в отзыве. Это обеспечивает немедленный logout, но делает проверку чуть тяжелее (обращение к Redis) и, по сути, возвращает состояние на сервер. Тем не менее, такой механизм необходим в системах, где безопасность выше удобства – пользователь должен иметь возможность немедленно завершить все свои сессии. Примечание: храните отозванные записи как минимум до исходного истечения токена, чтобы не выпустить токен снова с таким же jti (маловероятно, но лучше перестраховаться, используя достаточную энтропию для jti). - Rotation & Short Expiry: как упомянуто, короткий срок жизни вкупе с refresh-токеном. Пользователь нажимает “Logout” – клиент просто удаляет у себя JWT и refresh cookie. Даже если злоумышленник завладел JWT, он скоро протухнет. А refresh вы можете сделать одноразовым, т.е. при каждой выдаче нового JWT старый refresh гасить. Тогда повторное использование украденного refresh-токена будет отвергнуто. Этот метод применяют крупные IdP: при logout они либо помечают refresh недействительным (если есть централизованный storage), либо просто полагаются, что access-токены сами скоро истекут. Например, Google ID Token живут часы, а refresh-токен можно аннулировать при выходе из Google Account.
- Привязка к сессии пользователя на сервере: гибридный подход – сервер все-таки ведет таблицу активных JWT, выдавая при логине не один токен, а пару идентификатор сессии + JWT. При выходе сервер просто помечает сессию завершенной. Каждый JWT включает claim с ID сессии, и сервер при проверке может сверяться со своим хранилищем сессий. Это в значительной степени нивелирует идею stateless, но иногда оправдано, если нужно принудительно разлогинивать по команде, не дожидаясь exp. Можно хранить только минимальную информацию – например, время выдачи токена, чтобы одновременно обезвредить и refresh, и все access токены этой сессии.
В любом случае, полный logout из распределенной системы с JWT – нетривиальная задача, и в протоколах это решается частично. Например, OpenID Connect предусмотрел специальный Front-Channel Logout и Back-Channel Logout, когда IdP уведомляет все участники о том, что пользователь вышел, чтобы те почистили свои сессии. Если вы контролируете все компоненты, вы можете спроектировать нечто подобное, но зачастую достаточно соединения “короткий токен + опциональный черный список”.
Дополнительные рекомендации:
- Следите за обновлениями библиотек JWT. Были случаи, когда популярные библиотеки имели уязвимости (например, отсутствие проверки алгоритма, ошибки при валидации). Используйте проверенные решения и своевременно их обновляйте.
- Настройте правильную обработку ошибок JWT. Не разглашайте клиенту лишних деталей (например, что именно неверно – подпись или формат). Просто возвращайте 401/403.
- Если возможно, соединяйте JWT с TLS-соединением: всегда передавайте токены только по HTTPS. Без этого их перехват (MitM) полностью компрометирует аккаунт.
- Учтите размер JWT: слишком много или слишком большие claims увеличат размер заголовков и могут приводить к проблемам (cookie не должны превышать ~4KB обычно). Храните только то, что нужно. Например, не стоит пихать в токен большой avatar image в base64 – лучше хранить URL или ID.
- При использовании JWT для авторизации в WebSocket или других нестандартных каналах – также соблюдайте осторожность. Например, не передавайте JWT как GET-параметр (URL) без необходимости, т.к. он может попасть в логи.
Совокупность этих мер поможет избежать самых распространенных проблем безопасности с JWT. Многие из них собраны в OWASP JWT Cheat Sheet.
Заключение
JSON Web Token (JWT) – это мощный и гибкий инструмент, который при правильном использовании позволяет упростить архитектуру аутентификации, повысить масштабируемость системы и избавиться от хранения состояния сессий на сервере. Он идеально подходит для распределённых приложений, REST API и микросервисных систем, особенно в сочетании с протоколами OAuth 2.0 и OpenID Connect.
Однако за удобством JWT скрываются и подводные камни, игнорирование которых может привести к серьёзным уязвимостям: от компрометации пользовательских данных до полного обхода аутентификации. Как мы увидели, важно не только правильно сформировать токен, но и грамотно его валидировать – с учетом срока действия, издателя, аудитории и подписи. При этом подходы будут отличаться в зависимости от того, вы создаете токен сами или получаете его от внешнего провайдера.
В этой статье мы:
- Подробно разобрали структуру JWT и различия между JWS и JWE;
- Сравнили token-based и session-based модели;
- Показали, как происходит генерация и проверка токена в Spring Boot и Go (Gin);
- Рассмотрели все шаги валидации, включая работу с JWKS;
- Разобрали уязвимости и best practices при работе с JWT.
В реальных системах JWT редко применяется в одиночку. Он становится частью более широкой экосистемы: авторизационных серверов, прокси, middleware, Identity Provider’ов и систем контроля доступа. Поэтому понимание принципов работы JWT на низком уровне даёт разработчику уверенность и гибкость при проектировании сложных решений.
Если вы внедряете JWT в production – обязательно следуйте рекомендациям OWASP, соблюдайте минимизацию данных в токене, используйте короткие сроки жизни, и обеспечьте защиту от XSS/CSRF. Помните: подпись – не шифрование, и с этим нужно жить осознанно.
Полезные ссылки и источники
- jwt.io – официальный сайт и библиотека для работы с JWT, интерактивный декодер токенов.
- RFC 7519: JSON Web Token – спецификация JWT, описывает формат и зарегистрированные claims.
- OWASP Cheat Sheet: JSON Web Token – рекомендации по безопасному использованию JWT (на англ., содержит частные случаи для Java и общие советы).
- RFC 7515, RFC 7516, RFC 7517 – спецификации JWS, JWE и JWK/JWKS, соответственно (для глубокого понимания типов токенов и форматов ключей).
- Документация Spring Security – OAuth2 Resource Server – как настроить проверку JWT в Spring (на сайте Spring).
- GitHub golang-jwt/jwt – библиотека JWT для Go, README содержит примеры кода.
You must be logged in to post a comment.