Пользователь один раз ввёл логин и пароль — а дальше каждый его запрос к десятку сервисов как-то опознаётся, без повторного ввода пароля. Делают это токены. Разберём с нуля: что такое токен 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
Сервис делает так:
- Один раз при старте (или при первом запросе) скачивает ключи с JWKS-адреса и держит их в памяти (кеширует).
- На каждый входящий запрос берёт подпись из токена и проверяет её скачанным публичным ключом.
- Если подпись сходится — токен настоящий. Если нет — отклоняет запрос.
Запроса к 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-токенов.