← назад к разделу

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

Что такое токен и зачем он нужен

Представьте пропуск в большое здание. На входе охранник проверил ваш паспорт (логин и пароль) и выдал бейдж. Дальше вы ходите по этажам и показываете не паспорт, а бейдж — его проверка быстрая, и паспорт каждый раз доставать не нужно.

Токен — это и есть такой бейдж. Keycloak (сервер, который проверяет пользователей) после успешного входа выдаёт два токена:

  • access token — короткоживущий «бейдж». Его прикладывают к каждому запросу к сервисам. Живёт обычно несколько минут.
  • refresh token — «талон на продление». По нему получают новый access token, когда старый протухает, не заставляя пользователя снова вводить пароль. Живёт дольше — часы или дни.

Access token приходит в каждом запросе в заголовке:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI...

Слово Bearer означает «предъявитель»: кто принёс токен, тот и считается его владельцем. Поэтому к хранению токенов относятся серьёзно — об этом в конце.

Из чего состоит JWT

Access token у Keycloak — это JWT (JSON Web Token). Выглядит как длинная строка из трёх частей, разделённых точками:

xxxxx.yyyyy.zzzzz
   |     |     |
header payload signature

Каждая часть — это закодированный (base64url) кусок данных:

  • header (заголовок) — служебная информация: какой алгоритм подписи используется (RS256) и каким ключом (kid — идентификатор ключа). Пример: {"alg":"RS256","typ":"JWT","kid":"abc123"}.
  • payload (содержимое) — собственно данные о пользователе и токене. Эти данные называют claims (утверждения).
  • signature (подпись) — криптографическая печать. Она доказывает, что токен выпустил именно Keycloak и что его не подменили по дороге.

Важно понять: первые две части не зашифрованы, а просто закодированы. Любой может их раскодировать и прочитать (например, на сайте jwt.io). Никаких паролей и секретов в payload класть нельзя — там только то, что не страшно показать.

Типичный payload от Keycloak:

{
  "iss": "https://keycloak.example.com/realms/shop",
  "sub": "a1b2c3d4-...",
  "exp": 1735689600,
  "iat": 1735689300,
  "preferred_username": "ivan",
  "realm_access": { "roles": ["customer"] },
  "scope": "openid profile email"
}

Что значат основные claims:

  • iss (issuer) — кто выпустил токен. Адрес вашего realm в Keycloak. Realm — это изолированное «царство» пользователей и настроек; в одном Keycloak их может быть много.
  • sub (subject) — кто пользователь. Неизменный идентификатор, по нему сервис понимает, чей это запрос.
  • exp (expiration) — до какого момента токен действителен (время в секундах с 1970 года).
  • iat (issued at) — когда выпущен.
  • realm_access.roles — роли пользователя (например, customer, admin). По ним решают, что ему можно.

Как сервис проверяет подпись

Вот ключевая проблема: токен приходит от клиента, а клиенту доверять нельзя. Что мешает злоумышленнику самому собрать JWT, написать туда "roles": ["admin"] и отправить? Ничего — кроме подписи.

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

Публичные ключи лежат по специальному адресу — JWKS (JSON Web Key Set):

https://keycloak.example.com/realms/shop/protocol/openid-connect/certs

Сервис делает так:

  1. Один раз при старте (или при первом запросе) скачивает ключи с JWKS-адреса и держит их в памяти (кеширует).
  2. На каждый входящий запрос берёт подпись из токена и проверяет её скачанным публичным ключом.
  3. Если подпись сходится — токен настоящий. Если нет — отклоняет запрос.

Запроса к Keycloak на каждый входящий запрос при этом нет — ключи уже в памяти. Это быстро.

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

Что такое rotation ключей и почему это не ломает проверку

Время от времени Keycloak меняет ключ подписи — это называют rotation (ротация, обновление ключей). Делается для безопасности: даже если ключ когда-то утечёт, он скоро перестанет действовать.

Тут легко представить проблему: Keycloak сменил ключ — и все ранее выданные токены, подписанные старым ключом, разом стали «невалидными»? Нет. Keycloak делает это аккуратно:

  • новые токены подписываются новым ключом;
  • но на JWKS-адресе какое-то время лежат оба ключа — и новый, и старый;
  • старые токены ещё проверяются старым ключом, пока сами не протухнут по exp.

Здесь и нужен kid из header токена: сервис смотрит «каким ключом подписан этот токен» и берёт из набора именно его. Поэтому сервис должен скачивать весь набор ключей и при незнакомом kid — обновить набор с JWKS, а не падать. Готовые библиотеки делают это сами.

Срок жизни токена: claim exp

Проблема: бейдж нельзя выдавать навсегда. Если access token украдут, он не должен работать вечно.

За это отвечает exp. Сервис при проверке смотрит: текущее время больше exp? Тогда токен просрочен — запрос отклоняется. Срок жизни access token задаётся в настройках realm (часто 5–15 минут). Чем короче — тем безопаснее, но тем чаще придётся обновляться.

И тут возникает естественный вопрос: если access token живёт пять минут, пользователю что — каждые пять минут вводить пароль? Нет. Для этого есть refresh token.

Refresh token: обновление без повторного входа

Refresh token — это «талон на продление». Когда access token протух, клиент не идёт к пользователю за паролем, а отправляет refresh token на token-адрес Keycloak и получает свежий access token (и обычно новый refresh token).

Запрос на обновление:

POST https://keycloak.example.com/realms/shop/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&client_id=web-app
&refresh_token=<сохранённый refresh token>

В ответ — новый access token. Пользователь ничего не заметил.

Зачем такое разделение на два токена? Чтобы совместить две вещи, которые обычно противоречат друг другу:

  • access token короткий — если украдут, проживёт недолго и проверяется быстро (без похода в Keycloak);
  • refresh token длинный, но его предъявляют редко и только самому Keycloak — а Keycloak ведёт учёт сессий и может его в любой момент аннулировать.

Rotation refresh-токенов

Хорошая практика, которую Keycloak умеет включать, — rotation refresh-токенов: при каждом обновлении старый refresh token гасится и выдаётся новый. Если кто-то попробует второй раз воспользоваться уже использованным refresh-токеном, Keycloak видит повтор, считает это признаком кражи и аннулирует всю цепочку — и вора, и настоящего пользователя выкидывает. Лучше попросить войти заново, чем оставить доступ злоумышленнику.

Logout и отзыв сессии

Access token уже выдан и лежит у клиента в кармане. Как сделать так, чтобы он перестал работать до своего exp — например, когда пользователь нажал «Выйти» или администратор заблокировал аккаунт?

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

Способы завершить сессию:

  • Logout — клиент дёргает logout-адрес и передаёт refresh token. Сессия завершается, refresh token больше не работает:

    POST https://keycloak.example.com/realms/shop/protocol/openid-connect/logout
    
  • Отзыв токена — отдельный revoke-адрес гасит конкретный refresh или access token:

    POST https://keycloak.example.com/realms/shop/protocol/openid-connect/revoke
    
  • Завершение сессии администратором — из админ-консоли Keycloak. Все токены этой сессии становятся недействительны.

Здесь есть тонкость, которую важно понимать. После logout refresh token гарантированно мёртв — он проверяется на стороне Keycloak. А вот уже выданный access token, который сервисы проверяют локально по подписи, может проработать ещё несколько минут — до своего exp. Сервис ведь не ходит в Keycloak на каждый запрос. Именно поэтому access token делают коротким: окно, в котором отозванный токен ещё «живой», должно быть маленьким. Если нужно гасить access token мгновенно, есть более тяжёлый механизм — проверка токена через introspection-адрес на каждый запрос, но это медленнее и используется реже.

Подключение проверки на стороне Spring

В Spring всё это делает стартер spring-boot-starter-oauth2-resource-server — он берёт на себя скачивание JWKS, проверку подписи и exp. Своими руками распаковывать JWT не нужно.

Достаточно конфигурации:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://keycloak.example.com/realms/shop

По issuer-uri Spring сам находит JWKS-адрес (через .well-known/openid-configuration) и проверяет каждый входящий токен: подпись, срок exp и совпадение iss. Метаданные и ключи запрашиваются не на старте, а при первом запросе с токеном, поэтому недоступность Keycloak в момент запуска приложение не роняет. Если .well-known/openid-configuration недоступен или адрес ключей хочется задать явно (Keycloak за прокси, нестандартный путь), вместо issuer-uri указывают jwk-set-uri напрямую:

          jwk-set-uri: https://keycloak.example.com/realms/shop/protocol/openid-connect/certs

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

Типичные ошибки с токенами

Большинство уязвимостей — это не «взлом криптографии», а простые недосмотры.

  • Не проверять подпись. Сервис раскодировал payload, прочитал роли — и поверил. Без проверки подписи клиент может прислать любой токен с "roles": ["admin"]. Подпись проверяется всегда.
  • Хранить токены в localStorage. В браузерном хранилище localStorage токен доступен любому скрипту на странице. Одна уязвимость типа XSS (когда на страницу попадает чужой скрипт) — и токен утёк. Безопаснее хранить токен в cookie с флагами HttpOnly (скрипту не виден), Secure (только по HTTPS) и SameSite.
  • Путать аутентификацию и авторизацию. Это разные вопросы. Аутентификация — «кто ты» (подпись и exp валидны). Авторизация — «что тебе можно» (есть ли нужная роль). Валидный токен ещё не значит, что его владельцу можно удалять чужие заказы. Роль и право на конкретный ресурс надо проверять отдельно.
  • Слишком длинный exp. Соблазнительно поставить access token срок жизни в сутки, чтобы реже обновлять. Но тогда украденный токен сутки и проживёт, а отозвать его до exp нельзя. Access token держат коротким, а удобство дают через refresh token.
  • Доверять claims без валидации. Даже у токена с верной подписью надо проверять iss (выпустил именно ваш realm, а не чужой Keycloak) и, где это применимо, aud (токен предназначен именно вашему сервису). Иначе токен от соседнего сервиса может пройти там, где не должен.

Коротко

  • Keycloak выдаёт два токена: короткий access token (прикладывается к каждому запросу) и длинный refresh token (для продления без повторного входа).
  • Access token — это JWT из трёх частей: header, payload (claims), signature. Первые две не зашифрованы, секреты в них не кладут.
  • Подпись проверяют публичным ключом из набора JWKS; ключи кешируются в памяти, на каждый запрос в Keycloak не ходят.
  • Rotation ключей не ломает проверку: на JWKS какое-то время лежат старый и новый ключ, токен подбирается по kid.
  • exp задаёт срок жизни; протух — запрос отклоняется. Обновление — через refresh token на token-адрес.
  • Logout и revoke завершают сессию; access token при этом может доработать до exp, поэтому его делают коротким.
  • В Spring подпись и exp проверяет spring-boot-starter-oauth2-resource-server по issuer-uri или jwk-set-uri.
  • Частые ошибки: не проверять подпись, хранить токен в localStorage, путать аутентификацию с авторизацией, слишком длинный exp, доверять claims без проверки iss/aud.

Что почитать дальше

  • JWT validation в Spring Security — oauth2ResourceServer, кеш JWK Set и почему 401, а не 403.
  • RBAC: маппинг ролей JWT — как realm_access.roles превращаются в проверки доступа.
  • Хранение токенов на клиенте — HttpOnly cookie вместо localStorage и rotation refresh-токенов.