Валидация JWT-токена в Spring Boot с Keycloak

Оглавление

  1. Введение
  2. Основы JWT: структура и алгоритмы подписи
  3. Специфика JWT-токенов в Keycloak: Realm, Roles, Scopes
  4. Валидация JWT access-токена в Spring Boot (с публичным ключом Keycloak)
  5. Обновление токена: использование Refresh Token
  6. Авторизация на основе ролей и scope (Spring Security + Keycloak)
  7. Реализация: конфигурация Spring Boot и интеграция с Keycloak
  8. Заключение

Введение

JSON Web Token (JWT) – это компактный, самодостаточный токен, широко используемый для передачи данных аутентификации и авторизации между приложениями. В связке Java Spring Boot и Keycloak (выступающем как отдельный сервер аутентификации) JWT-токены служат ключевым механизмом подтверждения личности пользователя и проверки его прав доступа. В данной статье мы рассмотрим теоретические основы JWT, специфику токенов Keycloak (realm, scopes, roles), а также подробно разберем, как Spring Boot приложение валидирует access token (JWT токен доступа) с помощью публичного ключа Keycloak и как обрабатывается refresh token (токен обновления) для продления сессии пользователя. Кроме того, объясним, как Spring Security выполняет авторизацию на основе ролей и scope, настроенных в Keycloak, и покажем фрагменты кода конфигурации.

Основы JWT: структура и алгоритмы подписи

Структура JWT. JWT состоит из трех частей: заголовка (Header), полезной нагрузки (Payload) и подписи (Signature), разделенных точками. Заголовок обычно содержит тип токена (тип: JWT) и алгоритм подписи. Полезная нагрузка содержит собственно claims – набор утверждений о пользователе и токене (например, идентификатор пользователя, роли, время истечения и т.д.). Подпись служит для гарантии целостности токена и проверки его подлинности. Формат токена выглядит как строка из трех сегментов в Base64URL кодировке: Header.Payload.Signature.

Подпись и алгоритмы. В заголовке JWT указано, каким алгоритмом создана подпись токена, например HS256 или RS256. При HS256 используется HMAC-SHA256 с симметричным секретным ключом, известным и издателю, и проверяющей стороне. В случае RS256 (RSA-SHA256) применяется асимметричная криптография: сервер аутентификации (Issuer) генерирует пару ключей – приватный ключ для подписания и публичный ключ для проверки подписи. Это означает, что приложение-клиент может валидировать JWT, имея доступ только к публичному ключу издателя – сам секрет (приватный ключ) ему не нужен. Keycloak по умолчанию подписывает токены алгоритмом RS256, поэтому валидация токена в Spring Boot сводится к проверке RSA-подписи с использованием публичного ключа Keycloak, который можно безопасно получить из сервера авторизации.

Примеры полей (claims). В Payload токена содержатся как стандартные, так и произвольные поля. К стандартным относятся, например: iss (issuer, издатель токена, обычно URL Keycloak realm), sub (subject, идентификатор пользователя), exp (expiration time, время истечения токена), aud (audience, аудитория токена – например, клиент, для которого он выдан). Эти поля помогают при валидации – приложение проверяет, что токен выпущен нужным издателем и для нужного клиента, и что он не истек. Помимо стандартных, туда могут включаться дополнительные сведения о пользователе (имя, email) и его правах (например, роли или права доступа).

Специфика JWT-токенов в Keycloak: Realm, Roles, Scopes

Realm в Keycloak. Realm – это пространство имен (домена безопасности) в Keycloak, внутри которого находятся пользователи, клиенты (приложения) и настройки аутентификации. JWT-токен, выдаваемый Keycloak, всегда принадлежит определенному realm – это видно по полю iss (например, iss: “https://auth-server/realms/myrealm”). Realm определяет контекст удостоверений: пользователи и роли из одного realm по умолчанию не действительны в другом.

Роли (Roles). Keycloak оперирует двумя видами ролей: Realm Roles (глобальные для данного realm) и Client Roles (привязаны к конкретному клиенту/приложению). Информация о ролях, которые присвоены пользователю, вносится в JWT токен доступа в виде специальных claim. Обычно в токене Keycloak присутствует claim realm_access с вложенным JSON, содержащим массив ролей realm пользователя, например: “realm_access”: { “roles”: [“user”,”admin”] }. Также для каждого клиента могут быть указаны роли в resource_access. Например, для клиента с ID my-app токен может содержать: “resource_access”: { “my-app”: { “roles”: [“editor”] } }. Таким образом, приложение может вычитать из токена как realm-роли, так и роли, специфичные для своего клиента. Преимущество JWT в том, что все эти роли уже внутри токена – ресурс-сервер может сразу их использовать для авторизации без дополнительных запросов к Keycloak.

Scopes (OAuth2 scopes и client scopes). В контексте Keycloak слово scope может означать OAuth2 scope – строковое обозначение прав доступа, запрашиваемых клиентом. Обычно OAuth2 scope отображаются на определенные роли или разрешения. Keycloak также вводит понятие Client Scope – это группа настроек, которая может включать определенные маппинги ролей/клеймов в токен. Например, через Client Scope можно настроить, какие роли или утверждения попадут в токен, когда клиент запрашивает определенный scope. В самом JWT токене от Keycloak иногда присутствует поле scope (список scope через пробел) – например, “scope”: “profile email” – отражающее, какие OpenID Connect scopes были запрошены. Однако чаще для целей авторизации в Spring Boot используются именно роли (realm или client roles) из токена, а не строковые scope. Scopes в Keycloak помогают ограничивать содержимое токена: если у клиента отключен параметр Full Scope Allowed, то в токен попадут только роли, связанные с явно указанными scope (Client Scopes), иначе же токен включает все роли пользователя по умолчанию.

Refresh Token в Keycloak. Помимо access token, Keycloak выдает при аутентификации и refresh token. В отличие от access token, который обычно JWT и имеет короткий срок жизни (несколько минут), refresh token – это долгоживущий токен, предназначенный только для получения новых access token без повторного входа пользователя. Refresh token в Keycloak по умолчанию имеет более длительный срок (например, 30 минут или больше, в зависимости от настроек realm). Важно отметить, что refresh token – не предназначен для прямой отправки ресурс-серверу при каждом запросе. Он должен храниться на стороне клиента (например, фронтенда) и использоваться для запроса нового access token по специальному endpoint Keycloak. Как правило, refresh token также имеет формат JWT и выпускается тем же issuer, но он содержит специальные scope/роль offline_access, позволяющую Keycloak отличить его от обычного access token.

Валидация JWT access-токена в Spring Boot (с публичным ключом Keycloak)

Когда frontend-приложение получило от Keycloak JWT access token и использует его при обращениях к нашему Spring Boot backend (ресурс-серверу), задача бэкенда – проверить валидность этого токена. Ключевой момент: приложение не владеет секретным ключом, которым подписан токен, поэтому оно не может переподписывать или расшифровывать JWT; вместо этого оно проверяет подпись с помощью публичного ключа издателя (Keycloak). Благодаря алгоритму RS256 эта схема безопасна: приватный ключ известен только Keycloak, а публичный ключ можно раскрыть всем желающим.

Spring Boot предоставляет встроенную поддержку для JWT-проверки токенов через модуль spring-boot-starter-oauth2-resource-server. Настроив в application.properties информацию об издателе токена или URL для получения ключей, мы можем позволить Spring Security автоматически загружать публичные ключи Keycloak и валидировать подпись. Например, достаточно указать:

spring.security.oauth2.resourceserver.jwt.issuer-uri=https://auth-server/realms/myrealm 
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://auth-server/realms/myrealm/protocol/openid-connect/certs 

Параметр issuer-uri сообщает Spring Security, какого издателя он должен ожидать в токенах и позволяет автоматически скачать конфигурацию OpenID Connect (которая содержит ссылку на JWKS), а jwk-set-uri указывает напрямую URL JWKS (JSON Web Key Set) с публичными сертификатами Keycloak. Эти сертификаты содержат открытые ключи (в формате RSA) с идентификаторами ключей (kid), соответствующие приватным ключам, которыми подписываются JWT. При первом обращении к защищенному ресурсу Spring Boot скачивает JWKS и кеширует его для последующего использования.

Когда приходит HTTP-запрос с заголовком Authorization: Bearer <JWT>, цепочка фильтров Spring Security автоматически перехватывает его и выполняет следующие шаги:

Рис. 1: Обработка запроса с JWT-токеном в Authorization заголовке и валидация токена через публичный ключ Keycloak.

  1. Извлечение токена. Фильтр BearerTokenAuthenticationFilter (или эквивалентный) находит JWT в заголовке запроса и передает его в компонент валидации. Если токена нет или формат неверный – запрос отклоняется сразу с ошибкой 401.
  2. Получение публичного ключа (при необходимости). По kid (Key ID) из header токена приложение определяет, какой публичный ключ использовать для проверки подписи. Spring Security автоматически сопоставляет kid с ключами из ранее загруженного JWKS. Если нужного ключа еще нет, клиент может выполнить запрос к JWKS endpoint Keycloak (…/protocol/openid-connect/certs) и обновить набор ключей. Это не требует аутентификации и доступно публично, т.к. содержит только открытые ключи. Публичный ключ кешируется и переиспользуется, что избавляет от обращения к Keycloak на каждый запрос.
  3. Проверка подписи. Backend вычисляет криптографическую подпись на основе полученного токена (его header+payload) и сравнивает ее с подписью из токена, используя публичный ключ. Если подпись не совпадает – токен поддельный или поврежденный, и верификация проваливается (приложение должно отклонить запрос с ошибкой 401 Unauthorized). При совпадении подписи считается, что токен выпущен доверенным сервером (Keycloak).
  4. Проверка стандартных claim. Далее происходит базовая валидация содержимого JWT: не истек ли токен (exp), соответствует ли издатель (iss) ожидаемому адресу нашего realm, предназначен ли токен для данного сервиса (aud или azp – audience/authorized party) и т.д. Эти проверки выполняются автоматически, если настроен issuer-uri – Spring Security отклонит токен, если iss не совпадает с указанным URL realm. Также проверяется, что токен – тип access token (в Keycloak токенах есть поле typ или azp).
  5. Создание Authentication объекта. Если подпись и срок действия токена в порядке, Spring Security создает объект аутентификации (Authentication) на основе полученного JWT – по умолчанию это JwtAuthenticationToken. Этот объект содержит токен JWT и его payload (claims). На этом этапе можно сконфигурировать маппинг claim токена в “полномочия” (Granted Authorities) пользователя в приложении – об этом расскажем ниже. Authentication помещается в SecurityContext, и запрос считается аутентифицированным.
  6. Допуск к ресурсу или отказ. После успешной аутентификации фильтр передает управление дальше – к самому REST-контроллеру или сервису, обрабатывающему запрос. Если же на любом из этапов проверка не пройдена (например, подпись неверна, токен истек или отсутствует) – цепочка прерывается, и клиент получает ответ 401 Unauthorized (неавторизован) без передачи управления бизнес-логике.

Обратите внимание: такой подход проверки JWT не требует удаленного вызова к Keycloak для каждого запроса – все необходимые данные (права доступа, идентификатор пользователя и пр.) уже находятся внутри JWT. Запрос к серверу авторизации происходит только однажды для получения публичных ключей (JWKS), либо при их смене (Keycloak может периодически менять ключ подписи, указывая новый kid, тогда приложение заново скачает JWKS). Это сильно повышает производительность, позволяя авторизовать запросы локально. Альтернативой является механизм интроспекции токена, когда ресурс-сервер при каждом запросе обращается к специальному endpoint Keycloak /token/introspect, отправляя туда токен и получая информацию, активен он или отозван. Однако интроспекция требует сетевого вызова и наличия client credentials, поэтому в случае JWT предпочтительнее использовать локальную валидацию через публичный ключ.

Разберем еще раз более подробно, что именно вычисляется, когда мы проверяем JWT без похода в Keycloak и без приватного ключа.

1. Что у нас есть на старте

У нас есть две вещи:

  1. JWT-токен в заголовке: Authorization: Bearer <header>.<payload>.<signature>
  2. Публичный ключ Keycloak, который мы заранее забрали по JWKS: { "kty": "RSA", "kid": "abc123", "n": "base64url(modulus)", "e": "base64url(exponent)" }

По n и e мы можем восстановить RSAPublicKey. Приватный ключ (которым Keycloak подписывает) нам не нужен.

2. Как устроен JWT на уровне байт

Токен в компактном виде – это строка вида:

<base64url(header-json)>.<base64url(payload-json)>.<base64url(signature-bytes)>

Примерно:

eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJzdWIiOiIxMjM0NSIsInJvbGVzIjpbImFkbWluIl19.UdZ...Q
  • header: {"alg":"RS256","kid":"abc123","typ":"JWT"}
  • payload: {"sub":"12345","roles":["admin"],"exp":...}
  • signature: бинарная подпись (байты RSA), закодированные через base64url.

3. Подготовка: как мы получаем public key из JWKS

Это происходит один раз, либо при смене ключа.

У нас есть запись JWKS:

{
  "kty": "RSA",
  "kid": "abc123",
  "n": "sXb7...base64url...",
  "e": "AQAB"
}

В Java это превращается в PublicKey так:

byte[] nBytes = Base64.getUrlDecoder().decode(n); // modulus
byte[] eBytes = Base64.getUrlDecoder().decode(e); // exponent

BigInteger modulus = new BigInteger(1, nBytes);
BigInteger exponent = new BigInteger(1, eBytes);

RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
KeyFactory kf = KeyFactory.getInstance("RSA");
PublicKey publicKey = kf.generatePublic(spec);

Эту операцию за нас делает библиотека (Nimbus в Spring Security). Мы просто знаем: из JWKS ? RSA PublicKey.

4. Проверка подписи: что мы реально считаем

Алгоритм RS256 = RSASSA-PKCS1-v1_5 + SHA-256.

  1. Разбираем токен:
String[] parts = jwt.split("\\.");
String headerPart = parts[0];   // base64url
String payloadPart = parts[1];  // base64url
String signaturePart = parts[2];// base64url
  1. Декодируем header и достаем kid:
String headerJson = new String(Base64.getUrlDecoder().decode(headerPart), StandardCharsets.UTF_8);
// парсим JSON, достаем "kid" и "alg"

По kid находим нужный public key в кеше (по сути: Map<kid, PublicKey>).

  1. Готовим “сообщение”, которое подписывал Keycloak:
String signedContent = headerPart + "." + payloadPart;
byte[] signedBytes = signedContent.getBytes(StandardCharsets.US_ASCII);

Важно: подписывается НЕ JSON, а именно та строка base64url(header) + "." + base64url(payload).

  1. Декодируем подпись:
byte[] signatureBytes = Base64.getUrlDecoder().decode(signaturePart);
  1. Запускаем крипту:
Signature sig = Signature.getInstance("SHA256withRSA"); // RS256
sig.initVerify(publicKey);              // тот самый из JWKS
sig.update(signedBytes);                // header.payload
boolean valid = sig.verify(signatureBytes);

Под капотом происходит следующее:

  • считается hash = SHA-256(signedBytes);
  • выполняется RSA-проверка: расшифровывается подпись S с публичным ключом ? получается структура, в которой должен лежать тот же hash;
  • если они совпадают и структура корректна – подпись валидна.

Ключевой момент: вся математика делается локально, используя public key. Никаких обращений в Keycloak тут нет.

5. Проверка claims: что еще проверяется, кроме подписи

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

String payloadJson = new String(Base64.getUrlDecoder().decode(payloadPart), StandardCharsets.UTF_8);
// парсим JSON

Проверяем:

  • exp – не истек ли токен: long now = Instant.now().getEpochSecond(); if (exp < now) { reject(); }
  • iss – совпадает ли с нашим realm: if (!"https://auth.example.com/realms/MyRealm".equals(iss)) { reject(); }
  • aud / azp – токен предназначен именно для нашего клиента/ресурс-сервера;
  • typ / scope / roles – нужный тип токена (access token, а не refresh), набор ролей и т.д.

Эти проверки Spring Security делает сам, если мы настроили issuer-uri. Но концептуально это обычная проверка значений полей в payload.

6. Как это выглядит в Spring Security (упрощенно)

Все вышеописанное делает, по сути, вот этот объект:

JwtDecoder decoder = NimbusJwtDecoder
     .withJwkSetUri("https://auth.example.com/realms/MyRealm/protocol/openid-connect/certs")
        .build();
Jwt jwt = decoder.decode(token); // внутри - вся магия: split, verify, claims.

В decode() происходит:

  1. split на header.payload.signature;
  2. чтение kid, поиск публичного ключа (с загрузкой JWKS при необходимости);
  3. Signature.getInstance("SHA256withRSA") и verify(...);
  4. парсинг JSON в Jwt с claims.

А дальше SecurityConfig уже из Jwt достает роли, маппит в GrantedAuthority и кладет в SecurityContext.

По итогу:

  1. Токен – это три base64url-части: h, p, s.
  2. Keycloak подписывает строку h + "." + p приватным RSA-ключом ? получается s.
  3. Наш сервис:
    • берет h и p,
    • собирает строку h + "." + p,
    • берет публичный ключ из JWKS,
    • считает SHA256 от строки,
    • с помощью RSA-проверки убеждается, что s соответствует этому хэшу.
  4. Если подпись валидна и поля (exp, iss, aud, …) ок – токен настоящий и еще живой.

Точка. Все, что делает Spring/Keycloak-адаптеры – это автоматизирует вышеописанные операции.

Обновление токена: использование Refresh Token

Access token имеет относительно короткий срок жизни (например, 5-15 минут), поэтому для долгосрочных сессий применяется refresh token. Когда access token истекает, клиент может получить новый, предъявив действующий refresh token. Keycloak поддерживает стандартный OAuth2 Refresh Token Grant – специальный flow для обновления токена без повторного входа пользователя.

Процесс обновления выглядит следующим образом:

Рис. 2: Последовательность действий при обновлении access token с помощью refresh token.

  1. Истечение токена доступа. Пользователь (или SPA-приложение на фронтенде) делает запрос к защищенному ресурсу, но обнаруживает, что получен ответ 401 Unauthorized. Это происходит, когда access token истек, и ресурс-сервер отклонил запрос. Поскольку у клиента есть refresh token, он может использовать его для обновления.
  2. Запрос на обновление. Клиент инициирует обращение к серверу Keycloak (endpoint токенов) с запросом типа refresh token grant. Технически это HTTP POST запрос к URL вида: https://<keycloak>/realms/<realm>/protocol/openid-connect/token, с полями в теле: grant_type=refresh_token, client_id=<ID клиента> (и client_secret=… для конфиденциальных клиентов), а также refresh_token=<текущий refresh token>. Этот запрос обычно отправляется самим фронтендом или бекендом от имени пользователя – важно передавать и учетные данные клиента, чтобы Keycloak убедился, что refresh token действительно выдан данному приложению.
  3. Валидация refresh token в Keycloak. Keycloak, получив запрос, проверяет валидность refresh token – не истек ли он и не был ли отозван. Refresh token обычно живет гораздо дольше access token (например, часы или дни), но может быть отозван администратором или при логауте пользователя. Если refresh token недействителен, Keycloak вернет ошибку (тогда клиенту придется перенаправить пользователя на повторный логин).
  4. Выпуск нового токена. При успешной проверке Keycloak генерирует новый access token (JWT) для пользователя, а также, в зависимости от настроек, может выдать новый refresh token (либо оставить старый действительным до конца его срока). Новый access token возвращается клиенту в ответе (HTTP 200) вместе с другими полями (как при обычной выдаче токена).
  5. Повтор запроса с новым токеном. Получив обновленный access token, клиент сохраняет его (заменяет устаревший) и повторяет исходный запрос к ресурсу, но уже с новым токеном в заголовке Authorization. На этот раз, если все корректно, ресурс-сервер валидирует новый JWT и выполняет запрос, вернув успешный результат (например, данные по API).
  6. Обновление refresh token (опционально). Если Keycloak выдал новый refresh token (Rotation стратегия), клиент должен сохранить и использовать его для следующего обновления. Если же refresh token остался тем же (Reuse стратегия), дальнейшие обновления продолжат использовать его до истечения. При повторном логине (или при полном истечении refresh токена) выдается новая пара токенов.

Важно отметить, что валидация refresh токена отличается от access токена: приложение не проверяет подпись refresh токена самостоятельно. Refresh токен предназначен только для сервера авторизации. Поэтому все взаимодействие с refresh токеном – отправить его в Keycloak и получить ответ – происходит между клиентом и Keycloak. Бэкенд может предоставить эндпоинт, проксирующий эту функцию (например, /refresh), но чаще refresh токен применяется напрямую в фронтенде (в SPA) с использованием OAuth2 библиотек.

Безопасность refresh токена критически важна: если злоумышленник завладеет refresh токеном, он сможет бесконечно получать новые access токены (пока refresh не истечет или не будет отозван). Поэтому refresh токены обычно хранятся более защищенно (например, HttpOnly Cookie или безопасное хранилище мобильного приложения), чем сами access токены, и могут иметь помеченную роль offline_access (для особенно долгоживущих offline токенов, которые могут жить недели и месяцы для постоянных сессий).

Авторизация на основе ролей и scope (Spring Security + Keycloak)

Получить и валидировать JWT – это только часть задачи. Далее приложение должно решить, разрешить ли конкретному пользователю доступ к запрашиваемому ресурсу. Spring Security интегрируется с Keycloak таким образом, что роли и права пользователя, вложенные в JWT, преобразуются в Granted Authorities – и могут использоваться в правилах доступа.

Маппинг ролей из JWT. В JWT токене Keycloak, как мы выяснили, роли пользователя находятся, например, в realm_access.roles и resource_access секциях. Однако по умолчанию Spring Security не знает, как их интерпретировать. Мы можем настроить конвертацию JWT -> Authorities. Например, определив бин JwtAuthenticationConverter, извлекающий из claim roles и добавляющий префикс для соответствия формату ролей в Spring. Ниже приведен пример такого конвертера:

@Bean
public JwtAuthenticationConverter jwtAuthConverter() {
    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(jwt -> {
        // Извлекаем список ролей realm из claim "realm_access"
        Map<String, Object> realmAccess = jwt.getClaim("realm_access");
        Collection<String> roles = (Collection<String>) realmAccess.getOrDefault("roles", Collections.emptyList());
        return roles.stream()
            .map(roleName -> "ROLE_" + roleName.toUpperCase()) // добавляем префикс "ROLE_"
            .map(SimpleGrantedAuthority::new)
            .toList();
    });
    return converter;
}

В этом примере роли из realm_access.roles превращаются в GrantedAuthority с префиксом ROLE_. Например, роль admin станет ROLE_ADMIN. Spring Security по соглашению ожидает, что роли имеют такой префикс при использовании аннотаций @PreAuthorize(“hasRole(‘ADMIN’)”) или методов типа .hasRole(“ADMIN”) в конфигурации. Можно также объединить роли из разных секций токена (realm и клиентских) в одну коллекцию Authorities.

Настройка SecurityFilterChain. В современном Spring Boot (версии 2.7+/3.x) настройка безопасности выполняется через компонент SecurityFilterChain. При интеграции с Keycloak через JWT, конфигурация минимально включает включение OAuth2 Resource Server поддержки и указание, что использовать JWT (а не opaque token). Например:

@Configuration
@EnableMethodSecurity(prePostEnabled = true) // для аннотаций @PreAuthorize
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable() // CSRF не нужен для REST API (только если есть браузерные формы)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")    // только ADMIN
                .requestMatchers("/user/**").hasAnyRole("USER","ADMIN") // USER или ADMIN
                .anyRequest().authenticated()  // остальное требует аутентификации
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt ->
                jwt.jwtAuthenticationConverter(jwtAuthConverter()) // используем наш конвертер ролей
            ));
        return http.build();
    }
}

В этом фрагменте мы отключаем CSRF для простоты (т.к. API будет использовать stateless токены), задаем правила доступа к URL-эндпоинтам на основе ролей (hasRole) и подключаем поддержку JWT-проверки. Метод jwtAuthenticationConverter вставляет наш бин конвертера, чтобы Spring Security знал, как из содержимого JWT создать Authentication с нужными правами. Таким образом, когда пользователь с ролью admin в токене обращается к /admin/* URL, у него будет authority ROLE_ADMIN и доступ будет разрешен, а если роль не соответствует – Spring выдаст ошибку 403 Forbidden автоматически.

Аннотации и методная безопасность. Помимо конфигурации URL, Spring Security позволяет ограничивать доступ непосредственно на методах контроллеров с помощью аннотаций. Благодаря включенному @EnableMethodSecurity, можно использовать аннотации вроде @PreAuthorize или @RolesAllowed на уровнях контроллеров или сервисов. Например:

@RestController
@RequestMapping("/api")
public class ProjectController {
    @GetMapping("/projects")
    @PreAuthorize("hasAuthority('ROLE_USER')")  // требуется роль USER
    public List<Project> listProjects() { ... }

    @PostMapping("/projects")
    @RolesAllowed("ROLE_ADMIN")  // или hasRole("ADMIN")
    public Project createProject(@RequestBody Project prj) { ... }
}

В данном примере метод listProjects будет доступен всем пользователям с ролью USER (или ADMIN, так как у admin обычно тоже есть ROLE_ADMIN, если настроили), а создание нового проекта (createProject) – только админу. При вызове этих методов Spring сравнивает роли из Authentication (которые появились там из JWT) с требуемыми. Если не хватает прав – бросается исключение AccessDeniedException, и пользователь получит ответ с кодом 403 Forbidden. Механизм авторизации полностью интегрирован: разработчик может сосредоточиться на проставлении нужных ограничений, а связка Spring Security + JWT из Keycloak позаботится об остальном.

Проверка прав доступа – пример последовательности. Ниже показан упрощенный сценарий авторизации запроса на основе ролей токена:

Рис. 3: Проверка прав доступа (авторизация) на примере доступа к ресурсу /admin.

На схеме видно, что если пользователь с недостаточными правами пытается обратиться к защищенному ресурсу, то после аутентификации JWT Spring Security выполняет шаг проверки авторизации и сразу возвращает отказ (403 Forbidden) без выполнения бизнес-логики в контроллере. Только пользователи с требуемой ролью проходят этот фильтр и получают ответ с данными.

В контексте Keycloak роли управляются централизованно: добавив или убрав роль у пользователя в Keycloak, мы тем самым изменим его доступ ко всем приложениям-ресурсам, которые доверяют токенам этого realm. Прелесть JWT в том, что новые роль/права вступят в силу для данного пользователя только при выдаче нового токена (старый токен все еще содержит старый набор ролей). Однако время жизни access token невелико, поэтому через несколько минут пользователь получит свежий токен с обновленными ролями. Если требуется немедленно отобрать доступ, можно отозвать сессию/refresh token в Keycloak – тогда даже не истекший access token станет недействительным при попытке интроспекции, но при локальной проверке JWT это не поможет. Поэтому важно подбирать разумные времена жизни токенов и при критических изменениях прав возможно вынуждать повторную аутентификацию.

Прежде чем заключить, рассмотрим общую архитектуру взаимодействия компонентов: пользователь, клиентское приложение, защищенный ресурс-сервер и Keycloak. Ниже представлена C4 Container диаграмма, иллюстрирующая роли каждого участника:

Рис. 4: Архитектура интеграции Spring Boot приложения с Keycloak (C4 Container диаграмма).

На диаграмме: Пользователь взаимодействует с фронтенд-приложением (например, одностраничное веб-приложение или мобильный клиент). Frontend перенаправляет пользователя на Keycloak для аутентификации (OIDC login), после чего получает от Keycloak токены (access token и refresh token). Далее при обращении к защищенному API Backend (Spring Boot приложение) фронтенд прикрепляет access token в заголовке. Backend, будучи настроен как ресурс-сервер, доверяет токенам, выпущенным Keycloak, и валидирует их (как мы обсудили ранее) через публичный ключ. Дополнительно, Backend при старте может автоматически скачивать метаданные OpenID Connect с Keycloak (по issuer-uri) – это показано как пунктирная связь: приложение узнает URL JWKS для проверки токенов. После успешной верификации JWT и проверки прав Backend выполняет запрошенные операции и возвращает данные фронтенду. Keycloak на этапе валидации запроса обычно не вызывается напрямую (если используется локальная проверка JWT), однако показан на диаграмме, так как первоначально именно он выдает токен и сообщает параметр issuer и ключи, по которым Backend доверяет токену.

Реализация: конфигурация Spring Boot и интеграция с Keycloak

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

Зависимости: Убедитесь, что в проект добавлен модуль Spring Security OAuth2 Resource Server и при необходимости библиотека для работы с JWT. В Gradle/Maven это обычно spring-boot-starter-security и spring-boot-starter-oauth2-resource-server. Для интеграции с Keycloak не требуется специфических библиотек Keycloak Adapter (начиная с Keycloak 17, ключковые адаптеры объявлены устаревшими, поскольку Spring Security обеспечивает нативную поддержку OAuth2/OpenID Connect).

Настройка application.properties: пропишите URL вашего Keycloak сервера и realm. Например:

# URL Keycloak realm (issuer)
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://auth.example.com/realms/MyRealm
# JWKS (публичные ключи). Можно не указывать явно, если issuer-uri указан (будет получено автоматически)
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://auth.example.com/realms/MyRealm/protocol/openid-connect/certs

Если ваш Keycloak требует https (как обычно и бывает), убедитесь, что используете https URL. При старте приложение попробует связаться с указанным issuer-uri для загрузки конфигурации (в том числе JWKS). Если Keycloak недоступен в этот момент (например, сети нет), приложение может выдать ошибку и не поднять контекст, поэтому в продакшене важно обеспечить доступность сервера авторизации.

Конфигурация безопасности: как обсуждалось, используйте компонент SecurityFilterChain. Например:

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
          .csrf().disable()
          .authorizeHttpRequests(auth -> auth
              .antMatchers("/api/public/**").permitAll()        // публичные ресурсы без токена
              .antMatchers("/api/admin/**").hasRole("ADMIN")    // только ADMIN
              .antMatchers("/api/**").authenticated()           // остальные API требуют аутентификации
          )
          .oauth2ResourceServer(oauth2 -> oauth2
              .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()))
          );
        return http.build();
    }

    // бин конвертера ролей (пример реализации приведен выше)
    @Bean
    public JwtAuthenticationConverter jwtAuthConverter() { ... }
}

Обратите внимание на некоторые моменты: мы используем permitAll() для некоторых URL (если нужны публичные эндпоинты), hasRole(“ADMIN”) и т.п. для ограничения доступа. Метод .oauth2ResourceServer().jwt() включает поддержку JWT-авторизации. Через jwt(…).jwtAuthenticationConverter(…) мы подключаем написанный конвертер, чтобы Spring Security знал, как получить GrantedAuthority из токена (например, роль admin превращается в ROLE_ADMIN). Если этого не сделать, по умолчанию Spring Security может привязать authorities на основе поля scope или authorities JWT, но Keycloak туда роли не кладет – он использует отдельные поля. Поэтому настройка конвертера – рекомендуемый шаг для корректной интеграции ролей.

Обработка refresh token: Так как refresh токен напрямую не обрабатывается фильтрами Spring (он не используется для доступа к ресурсам), работа с ним ложится на плечи клиентского кода. Если фронтенд – SPA, обычно в его логике прописано отслеживание истечения токена и запрос нового через Keycloak SDK или HTTP запрос. Если же обновление токена решено делать через наш бэкенд (например, мобильное приложение шлет refresh token на специальный эндпоинт нашего сервера), то нам нужно реализовать контроллер, который примет refresh token, вызовет Keycloak и вернет обратно новые токены. Это может выглядеть так:

@RestController
public class AuthController {
    @PostMapping("/api/refresh")
    public ResponseEntity<?> refreshToken(@RequestParam String refreshToken) {
        // Формируем запрос к Keycloak OpenID Connect token endpoint
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add("grant_type", "refresh_token");
        formData.add("client_id", "my-client");       // ID клиента в Keycloak
        formData.add("client_secret", "secret123");   // секрет, если конфиденциальный клиент
        formData.add("refresh_token", refreshToken);
        // Выполняем запрос (можно с RestTemplate, WebClient или HttpClient)
        TokenResponse response = keycloakTokenService.requestToken(formData);
        // возвращаем полученный новый access token (и refresh token, если есть)
        return ResponseEntity.ok(response);
    }
}

Здесь keycloakTokenService – условный сервис, который отправляет HTTP POST на https://<host>/realms/<realm>/protocol/openid-connect/token и парсит ответ JSON с токенами. В реальной реализации нужно обработать ошибки (например, просроченный или недействительный refresh token). Также важно защищать этот эндпоинт: по сути он открытый для неавторизованных (т.к. пользователь может не иметь действующего access токена, раз он пришел обновлять), поэтому нужно предусмотреть дополнительные меры (например, капча или ограничение по частоте, чтобы злоумышленник не пытался перебором эксплуатировать refresh).

Часто обновление токена выносится полностью на клиент: современные библиотеки (например, Keycloak JavaScript Adapter, OAuth2 client) могут автоматически обновлять токен и только сообщать приложению результат. Поэтому бекенд может вообще не получать refresh token – он лишь будет получать новые access token по мере их обновления.

Логирование и отладка. При настройке интеграции полезно добавить логирование JWT токенов и результатов их проверки. Spring Security предоставляет класс JwtDecoder – можно подключить свой, чтобы например, логировать содержимое токена. Но проще воспользоваться debug-режимом: при старте приложения включите logging.level.org.springframework.security=DEBUG – тогда в консоли можно увидеть шаги валидации, причины отказов (например, “Invalid signature” или “Expired token”) и т.д. А сам JWT всегда можно декодировать с помощью онлайн-сервисов или команды base64 (первые два сегмента).

Заключение

Интеграция Spring Boot приложения с Keycloak через JWT позволяет реализовать современную, безопасную и масштабируемую систему аутентификации и авторизации. JWT токены обеспечивают переносимость информации о пользователе и его правах, Spring Security предоставляет готовый каркас для их проверки и принятия решений об доступе, а Keycloak выступает надежным централизованным IDM, который выпускает токены и управляет учетными записями. Рассмотренный процесс – валидация JWT с помощью публичного ключа – гарантирует, что ресурс-сервер доверяет только токенам от своего провайдера. Использование refresh token позволяет поддерживать длительные сессии без повторного входа, но требует ответственного обращения. Настройки ролей и scope в Keycloak позволяют гибко контролировать, какая информация попадает в токен и каким будет уровень доступа пользователя.

Loading