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

Вы поставили Keycloak, открыли админку — и сразу налетела куча незнакомых слов: realm, client, client scope, mapper, группы, роли. Непонятно, что главнее, что во что вложено и с чего начинать. А пока модель в голове не уложилась, легко настроить так, что вход то работает, то нет, роли «не доезжают» до приложения, а почему — неясно. Давайте разберём модель Keycloak по кусочкам, не торопясь, с аналогиями из жизни, и в конце соберём минимальный рабочий набор для типичной связки «серверный бэкенд + браузерный фронтенд».

Главная развилка этой модели видна сразу: право, назначенное в админке, работает только тогда, когда оно попало внутрь токена.

роль назначена в админке — приложение читает её только из токена админка Keycloak access token realm marketplace пользователь ivan realm-роль: moderator client-роль backoffice: card:approve маппер realm-ролей маппер client-ролей realm_access.roles: ["moderator"] resource_access:backoffice.roles:["card:approve"] resource_access: {}client-ролей нет маппер включён — обе роли в токенебэкенд пускает к модерации по card:approve маппер client-ролей выключенв админке роль на месте, в токене её нет — 403 приложение не спрашивает Keycloak про роли на каждый запросчинить надо маппер или client scope, а не назначение роли

Назначение в админке и содержимое токена — не одно и то же: между ними стоит протокол-маппер. Выключен маппер client-ролей — resource_access приедет пустым, и card:approve для приложения просто не существует.

Обязательно

Зачем вообще отдельный сервер для входа

Прежде чем разбирать внутренности Keycloak, стоит понять, какую боль он лечит — иначе все эти realm и client кажутся лишним усложнением.

Представьте, что у компании одно приложение. Оно само хранит пароли пользователей, само проверяет логин, само решает, кому что можно. Пока приложение одно — терпимо. Но приложений становится два, потом пять. И начинается:

  • пользователя надо завести отдельно в каждом приложении;
  • человек поменял пароль в одном — в остальных остался старый;
  • единого входа нет: залогинился в одном приложении, в соседнее заходишь заново;
  • права (кто админ, кто обычный) описаны в каждом по-своему.

Keycloak — это отдельный сервер, который забирает всю эту работу себе. Он один на всю компанию хранит пользователей, проверяет пароли, умеет вход через Google или корпоративную учётную запись. А приложениям он выдаёт подтверждение: «да, это действительно Иван, и вот какие у него права». Приложения больше не возятся с паролями — они только спрашивают Keycloak и доверяют его ответу.

Ответ Keycloak отдаёт не в виде «да/нет», а в виде токена — подписанной строки, внутри которой записано, кто пользователь и что ему можно. Приложение проверяет подпись (подделать её без секретного ключа Keycloak нельзя) и доверяет содержимому, не дёргая Keycloak на каждый запрос. Это важная мысль, к которой мы ещё вернёмся: приложение читает права прямо из токена, а не ходит за ними в Keycloak.

Чтобы было видно, кто с кем общается, вот общая картина. На схеме: пользователь работает с приложением, приложение отправляет его логиниться в Keycloak, получает обратно токены и дальше ходит с ними в свой API.

Диаграмма

Теперь разберём, из чего Keycloak состоит внутри.

Realm — изолированное пространство

Самое верхнее понятие в Keycloak — это realm. С него начинается всё остальное, поэтому с него и начнём.

Представьте многоквартирный дом. Один установленный Keycloak — это весь дом. А realm — это отдельная квартира со своим замком. Жильцы одной квартиры физически не могут попасть в другую: у каждой квартиры свои ключи, свои жильцы, свои правила. Квартиры стоят в одном доме, но друг про друга ничего не знают.

Realm — это полностью изолированное пространство. Внутри него живут: свой набор пользователей, свои приложения (client'ы), свои роли, группы и настройки входа (например, через какие способы можно логиниться). Главное свойство — изоляция: пользователь, заведённый в одном realm, в другом realm просто не существует. Роль из одного realm в другом не действует. Это не «разные папки в одной системе», а действительно отдельные миры.

Зачем такая жёсткая изоляция? Затем, что на одном сервере Keycloak часто нужно держать совершенно разные аудитории, которые не должны пересекаться. Типичные примеры:

  • отдельно realm для сотрудников компании и отдельно realm для внешних клиентов — у них разные пользователи и разные правила;
  • отдельный realm под каждый независимый проект, чтобы они не мешали друг другу.

На схеме видно, что один Keycloak держит несколько realm, и они не связаны: пользователи и роли realm A никак не видны в realm B.

Диаграмма

Важная деталь, на которой спотыкаются новички: в Keycloak всегда есть встроенный realm с именем master. Возникает соблазн складывать пользователей приложения прямо в него — он же уже готовый. Так делать не надо. master нужен только для администрирования самого Keycloak: в нём живут учётки, которыми вы заходите в админку и управляете другими realm. Обычных пользователей вашего приложения туда не кладут. Под свой проект всегда создают новый realm — например, myapp.

Ещё одна вещь, которую полезно запомнить сразу: все адреса Keycloak привязаны к realm. Базовый адрес, по которому приложение находит ключи и настройки конкретного realm, выглядит так:

https://auth.example.com/realms/myapp

Этот адрес называют issuer (издатель токенов) — именно он стоит в каждом выданном токене как «кто меня выпустил». Запомните его: он понадобится при настройке приложения, и он у каждого realm свой.

Client — это приложение, которое обращается к Keycloak

Внутри realm живут client'ы. Это второе по важности понятие, и оно часто путает, потому что слово «client» здесь означает не пользователя, а приложение.

Откуда вообще берётся такое понятие? Keycloak должен как-то различать, какое именно приложение к нему пришло. Браузерный фронтенд, мобильное приложение и фоновый серверный сервис ведут себя по-разному, и доверять им можно по-разному. Чтобы их различать, для каждого приложения в realm заводят запись.

Client — это и есть запись о приложении внутри realm. У каждого приложения, которое пользуется этим Keycloak, есть свой client со своим идентификатором (client id) — например, myapp-frontend или myapp-backend. Когда приложение начинает процесс входа, оно представляется этим идентификатором: «здравствуй, Keycloak, я client myapp-frontend».

Ключевая характеристика, по которой client'ы делятся на два типа, — может ли приложение надёжно хранить секрет (то есть пароль самого приложения). От этого зависит, как именно приложение доказывает Keycloak, что оно — это оно.

Public client

Public («публичный») — это приложение, которое в принципе не может надёжно спрятать секрет. Почему не может? Потому что весь его код доступен пользователю:

  • код одностраничного сайта (React, Angular, Vue) целиком загружается в браузер — открой инструменты разработчика и читай;
  • мобильное приложение можно скачать и распаковать.

Любой «секретный пароль», зашитый в такой код, на самом деле виден всем желающим, а значит, он бесполезен как секрет. Поэтому у public client секрета нет вообще — Keycloak его и не требует.

Так настраивают: одностраничные сайты (React, Angular, Vue) и мобильные приложения.

Возникает резонный вопрос: если у приложения нет секрета, как тогда защитить вход от подмены? Для этого у public client обязательно используют PKCE. Идея простая: в самом начале входа приложение генерирует одноразовую секретную строку, держит её у себя и в конце предъявляет, доказывая, что именно оно — то самое приложение, которое начинало вход. Это закрывает дыру, через которую злоумышленник мог бы перехватить код авторизации и обменять его на токены вместо вас. Важная деталь: сам сервер по умолчанию не требует PKCE — он лишь поддерживает его, если приложение решит использовать. Так в Keycloak было раньше, так осталось и в 26-й версии. Библиотеки на фронтенде обычно используют, но полагаться на это не стоит. В настройках клиента в Keycloak есть поле для метода проверки (S256) — заполните его, и тогда вход без PKCE сервер просто отклонит.

Confidential client

Confidential («конфиденциальный») — это приложение, которое работает на сервере и может надёжно хранить секрет, потому что его код пользователю не виден: он крутится на вашем бэкенде, а не в браузере.

У такого client есть client secret — по сути, пароль самого приложения. Им бэкенд доказывает Keycloak: «я действительно client myapp-backend, вот мой секрет». Так настраивают серверные приложения: бэкенд на Spring Boot, сервис на Node.js и т. п.

Разница между двумя типами укладывается в одну табличку. На ней — кто чем доказывает свою подлинность и для каких приложений подходит.

Тип clientХранит секретЧем защищён обмен кода на токеныДля каких приложений
publicнетприложение себя не удостоверяет вообще; PKCE лишь связывает старт входа с обменомфронтенд (SPA), мобильные
confidentialдаclient secret — им приложение доказывает, что оно это оно (PKCE сверху не лишний)серверный бэкенд

Простое правило, которое стоит запомнить: код виден пользователю — public; код только на сервере — confidential.

Два поля, на которых спотыкаются все: redirect URI и Web origins

У записи о приложении есть два поля, из-за которых первое подключение обычно не работает. Оба про адреса, и ошибки в них дают разные, но одинаково загадочные симптомы.

Valid redirect URIs — список адресов, на которые Keycloak согласен вернуть пользователя после входа. Проверка нужна не для порядка: без неё злоумышленник подставил бы в запрос свой адрес и получил код входа вместо вас. Поэтому адрес из запроса сверяется со списком, и если он не подошёл, Keycloak не редиректит никуда, а показывает страницу с текстом Invalid parameter: redirect_uri. Это ошибка номер один при первом подключении, и ищут её не в коде: сверяют, что в списке стоит ровно тот адрес, который приложение отправляет, вместе со схемой, портом и путём. Один лишний или пропущенный слеш на конце — уже несовпадение.

Web origins — список источников, которым Keycloak разрешает обращаться к себе из браузера. Это про механизм проверки источника запроса (CORS), и касается он только фронтенда: одностраничное приложение само ходит на /token за токенами, а браузер перед этим спрашивает у Keycloak разрешения. Если источника нет в списке, браузер запрос не выпустит, и в консоли появится жалоба на CORS — а разработчик прочитает её как «Keycloak не отвечает» или «токен не выдаётся». Значение + в этом поле удобно: оно разрешает источники, выведенные из списка redirect URIs.

Проверить оба поля дешевле всего с самого начала: пройти вход руками в браузере и посмотреть, доходит ли дело до возврата и до запроса на /token. Что именно в этих полях допустимо ставить, а что открывает дыру, разбирает чек-лист ниже.

Service account — учётная запись самого приложения

Отдельный случай confidential client: приложение, которое ходит в другое приложение без человека. Ночная выгрузка, сервис заказов, вызывающий сервис оплаты, обработчик очереди. Пользователя здесь нет, а токен нужен.

Для этого у client включают Service accounts roles (вкладка «Settings», требует включённой «Client authentication»). Keycloak создаёт для такого клиента отдельную учётную запись с именем вида service-account-order-service — она видна в списке пользователей и ведёт себя как пользователь, только без пароля и без возможности войти в браузере. Роли вешают именно на неё: открыть client, вкладка «Service accounts roles», назначить нужные.

Дальше приложение получает токен по паре client_id и client_secret:

curl -s -X POST https://auth.example.com/realms/myapp/protocol/openid-connect/token \
  -d grant_type=client_credentials \
  -d client_id=order-service -d client_secret=$CLIENT_SECRET

В полученном токене sub указывает на эту служебную запись, а не на человека, поле preferred_username равно service-account-order-service, и по полю azp видно, какой клиент его получил. Принимающая сторона по этим признакам и отличает сервисный вызов от пользовательского.

Две вещи, которые тут делают неправильно. Роли назначают самому клиенту «по привычке» через realm-роли для всех — тогда любой сервис получает права любого; право дают узкое и именно этой служебной записи. И второе: у такого клиента выключают всё остальное — Standard flow, Direct access grants, — потому что человек через него входить не должен. Как этим пользуются на стороне вызывающего сервиса и что проверяет принимающий, разбирает статья про межсервисные вызовы.

Пользователи

Мы разобрали, где живут приложения. Теперь — про людей.

Пользователь (user) — это учётная запись человека внутри realm. У неё есть логин, пароль, email, имя, а также произвольные дополнительные поля, которые в Keycloak называют атрибутами (например, отдел или номер телефона). Именно пользователь проходит вход: вводит логин и пароль, а Keycloak их проверяет.

Откуда берутся пользователи? Способов несколько:

  • их создают вручную в админке Keycloak;
  • они регистрируются сами, если регистрация включена;
  • их подтягивают из внешнего каталога компании (например, LDAP или Active Directory);
  • они приходят из входа через Google и подобные внешние сервисы.

Важный момент: сам по себе пользователь никаких прав не несёт. «Иван существует» — это ещё не «Ивану что-то можно». Права пользователю дают роли — к ним и переходим.

Роли: realm-роли против client-ролей

Вот центральная развилка модели, которую важно понять как следует.

Проблема: запись «пользователь Иван» не отвечает на вопрос «что Ивану можно». Нам нужен способ сказать «Иван — администратор» или «Иван может оформлять заказы». Именно для этого существуют роли.

Роль — это метка с правом, которую вешают на пользователя. admin, manager, customer — типичные роли. Дальше приложение смотрит на роли в токене и решает, пускать ли пользователя в ту или иную часть. Например, в раздел администрирования пускаем только тех, у кого есть роль admin.

В Keycloak роли бывают двух видов, и разница между ними — частый источник путаницы, поэтому разберём её подробно.

Realm-роль — это роль, общая для всего realm. Она не привязана ни к какому конкретному приложению и имеет смысл во всём проекте сразу. Хорошие кандидаты в realm-роли — это общие, сквозные понятия: admin, user, manager. Если у вас одно приложение или несколько приложений, но право означает одно и то же везде, — это realm-роль.

Client-роль — это роль, которая принадлежит конкретному client (приложению). Она существует только в контексте этого приложения. Зачем такое нужно? Затем, что одно и то же слово в разных приложениях может значить разное. Роль manager в приложении myapp-backend и роль manager в приложении reports-app — это две независимые роли, они не пересекаются. У «менеджера» в основном приложении и «менеджера» в системе отчётов могут быть совершенно разные права, и client-роли позволяют их не смешивать.

Аналогия, которая хорошо ложится: realm-роль — это пропуск во всё здание, а client-роль — ключ от одной конкретной комнаты внутри этого здания.

Когда какие выбирать? Для маленького проекта обычно хватает realm-ролей — они проще: завёл admin и user, раздал пользователям, и всё. Client-роли берут, когда приложений несколько и права в них реально различаются настолько, что общие realm-роли начинают мешать.

Отдельно полезно знать про составные роли (composite). Это роль, которая включает в себя другие роли. Назначаешь пользователю одну такую роль — и вместе с ней он автоматически получает все вложенные. Классический пример: роль admin включает в себя роль user. Тогда любому администратору не нужно отдельно выдавать user — она приедет вместе с admin. Удобно, чтобы не дублировать назначения. Удобство это не бесплатное, и цену стоит знать заранее. Составная роль раскрывается в токен целиком: если admin включает пять ролей, а каждая из них ещё по три, в realm_access.roles приедут все пятнадцать, а не одна. Отсюда два последствия. Токен растёт, а он едет в заголовке каждого запроса и упирается в ограничения прокси — об этом статья про токены. И главное, становится непонятно, откуда у человека право: в токене видно payouts-approve, а по какой из вложенных ролей она приехала и кто её там выдал, по токену не сказать вовсе.

Второй слой той же проблемы — группы вместе с составными ролями. Роли приезжают из группы, группа наследует роли родительской группы, а роль внутри составная: в итоге у человека набор прав, который никто не может объяснить, не разложив вручную три уровня вложенности. Поэтому дисциплина такая: вкладывать роли не глубже одного уровня, не смешивать «роли через группы» и «составные роли» для одного и того же права, и держать в realm короткий список — разбор в статье про роли и доступ.

Группы — чтобы не раздавать роли по одной

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

Эту боль решают группы.

Группа (group) — это набор пользователей с общими ролями и атрибутами. Логика такая: вы назначаете роли группе один раз, а каждый пользователь, который попадает в эту группу, автоматически получает все её роли. Добавил человека в группу — он сразу получил нужные права. Перевёл в другую группу — набор прав поменялся. Не надо трогать роли у каждого вручную.

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

Чтобы не путать роли и группы, запомните разницу одной фразой: роль — это само право; группа — это удобный способ раздать пачку прав сразу многим пользователям.

Теперь соберём всю модель в одну картину. На схеме видно дерево: realm — корень, внутри него лежат client'ы, пользователи, группы и realm-роли; а client-роли «висят» уже под конкретным client'ом, не на уровне realm.

Диаграмма

Пунктирные стрелки на схеме читаются так: группы раздают своим участникам роли и содержат пользователей. Сплошные стрелки — это «что во что вложено».

Как роли попадают в токен

А вот частая точка, где всё ломается на практике. Вы назначили пользователю роль в админке, всё выглядит правильно — а приложение её «не видит» и не пускает. Причина почти всегда одна и та же: роль не попала в токен.

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

За то, что и в каком виде попадает в токен, отвечают два механизма.

Протокол-маппер (protocol mapper) — это правило вида «возьми вот это (роль, атрибут, email) и положи в токен под таким-то именем». Хорошая новость: мапперы для realm-ролей и для client-ролей в Keycloak есть из коробки и обычно включены по умолчанию. То есть в типовой настройке роли попадают в токен сами, без ручной работы. Но если кто-то их выключил или вы настраивали client вручную «с нуля» — вот тут роли и могут потеряться.

Client scope — это переиспользуемый набор мапперов. Вместо того чтобы вешать одни и те же мапперы на каждый client руками, их собирают в client scope и подключают к нужным client'ам разом. Это просто способ не повторять одну и ту же настройку много раз.

Теперь — куда именно роли ложатся в токене. Эти поля стоит знать наизусть, потому что именно на них смотрит приложение:

{
  "preferred_username": "ivan",
  "realm_access": {
    "roles": ["admin", "user"]
  },
  "resource_access": {
    "myapp-backend": {
      "roles": ["manager"]
    }
  }
}

Читается так:

  • realm-роли лежат в поле realm_access.roles — это плоский список общих ролей;
  • client-роли лежат в resource_access.<client-id>.roles — то есть сгруппированы по приложению, к которому относятся.

С client-ролями есть вторая причина пропажи, и она коварнее первой, потому что маппер при ней совершенно исправен. Keycloak кладёт в resource_access роли не всех подряд приложений, а только тех, которые попали в область действия этого токена. Пока у клиента включена настройка «Full scope allowed» — попадают все, и вопрос не встаёт. А вот как только её выключили (а выключают её как раз из соображений безопасности, чтобы токен не носил лишнего), в токен приедут роли только тех приложений, которые явным образом подключены к клиенту через client scope. Не подключили — роли в токене нет, хотя на пользователе она висит и маппер включён.

Практический вывод: если роль есть у пользователя, но в токене её нет — не ищите проблему в назначении роли (там всё в порядке). Смотрите в две стороны: выключенный или неподключённый маппер и — для client-ролей — область действия токена.

Как это читает приложение (Spring Boot)

Соберём концы: токен выдан, роли в нём есть — что делает с ним серверное приложение?

Серверное приложение выступает в роли resource server — буквально «сервер ресурсов». Оно само не занимается входом (логином, паролями, перенаправлениями) — этим занят фронтенд вместе с Keycloak. Бэкенду приходит уже готовый токен в заголовке Authorization: Bearer <access_token>, и его задача — только проверить этот токен и пустить или не пустить.

Минимум настройки для Spring Boot — указать issuer вашего realm:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/myapp

Что происходит дальше автоматически. По этому адресу Spring сам находит служебный файл .well-known/openid-configuration (его публикует Keycloak), оттуда узнаёт адрес с публичными ключами realm (JWKS), скачивает ключи и кэширует их у себя. После этого каждый входящий токен Spring проверяет локально: сходится ли подпись (теми самыми ключами) и тот ли издатель (issuer). Никакого обращения к Keycloak на каждый запрос не происходит — поэтому это быстро.

Одна типичная доработка, без которой роли не заработают. Spring по умолчанию не знает, что роли лежат именно в realm_access.roles — это формат, специфичный для Keycloak, а не общий стандарт. Поэтому к нему добавляют небольшой конвертер ролей: он достаёт роли из realm_access.roles и превращает их в привычные Spring права вида ROLE_admin. После этого работают обычные проверки доступа на эндпоинтах. Подробно этот конвертер разбирается в отдельной статье про защиту API — ссылка ниже. Чтобы связка была рабочей здесь и сейчас, вот он целиком — это действительно несколько строк:

@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter roles = new JwtGrantedAuthoritiesConverter();
    roles.setAuthoritiesClaimName("realm_access.roles");
    roles.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(roles);
    return converter;
}

Поддержка вложенного пути (realm_access.roles вместо просто roles) появилась в Spring Security 6.4; в более старых версиях тот же результат получают своим преобразователем, который сам читает Map из поля realm_access. И ROLE_ в префиксе не украшение: без него hasRole('admin') не найдёт роль, потому что этот метод сам дописывает ROLE_ к имени. Разбор этой путаницы и client-роли вместо realm-ролей — в статье про роли и доступ.

Минимум для связки «бэкенд + фронт»

Теперь, когда модель понятна по частям, соберём её в практический чек-лист. Для типичного приложения с серверным бэкендом и браузерным фронтендом нужно:

  1. Один realm под проект — например myapp. Именно свой, не master: в master живут администраторы самого Keycloak, и учётка оттуда открывает весь сервер.
  2. Два client'а в этом realm:
    • myapp-frontend — public, с PKCE, для одностраничного фронтенда: секрет в браузере спрятать негде, он оказался бы в JS-бандле у каждого посетителя;
    • myapp-backend — confidential, с client secret, для серверного API.
  3. Роли — для начала realm-роли, например admin и user. Client-роли подключают позже, если приложений станет несколько и права начнут различаться.
  4. Группы — по желанию: удобно, когда пользователей много и наборы прав типовые.
  5. Пользователи с назначенными ролями — напрямую или через группы.
  6. Проверить, что realm-роли реально попадают в токен (поле realm_access.roles) — мапперы для этого обычно уже включены, но убедиться стоит: иначе бэкенд увидит вошедшего пользователя без единой роли и ответит 403 на всё.
  7. На бэкенде — resource server с issuer-uri на ваш realm и конвертером ролей: без конвертера роли из токена не станут authorities Spring, и hasRole не сработает ни разу.

Этого достаточно, чтобы фронтенд логинил пользователя через Keycloak, получал токен и слал его в API, а бэкенд проверял токен и пускал пользователя по ролям.

Как это выглядит в маркетплейсе

Абстракции легче удержать на живом примере. Возьмём сквозной кейс сайта — маркетплейс: площадка, где продавцы размещают товары, покупатели их заказывают, а сотрудники площадки модерируют карточки и разбирают споры. Карта сервисов даёт шесть контекстов: Catalog, Order, Payment, Notification, Customer, Backoffice.

Realm — один. Соблазн сделать по realm на роль («покупатели», «продавцы», «сотрудники») выглядит логично, но ломается на первом же сценарии: продавец бывает и покупателем, а данные пользователя тогда придётся дублировать. Realm — это граница организации, а не граница роли. Поэтому realm один — marketplace. Отдельный realm понадобится только под что-то, что живёт своей жизнью: например, площадка в другой стране со своими правилами или партнёрский портал с чужими сотрудниками. А вот тестовый контур в отдельный realm не выносят: тогда тестовые учётки лежали бы в одной базе с боевыми. Тест разводят отдельным сервером Keycloak.

Client — по приложению, а не по человеку. У маркетплейса их несколько, и типы разные:

ClientТипКто ходитПочему такой тип
storefront-webpublic + PKCEпокупатель в браузересекрет в браузере хранить негде
seller-cabinetpublic + PKCEпродавец в браузерето же самое
mobile-apppublic + PKCEпокупатель в приложениисекрет в APK не спрячешь
backofficeconfidentialсотрудник площадкисерверное приложение, секрет лежит на сервере
order-serviceconfidentialсам сервис заказовходит в Payment без участия человека

Первые четыре — про вход человека. Последний — про вызов сервиса сервисом: у Order Service нет пользователя, он получает свой токен по client credentials и им обращается к Payment.

Роли: что realm, а что client. Realm-роль отвечает на вопрос «кто этот человек на площадке вообще»:

  • buyer — покупатель;
  • seller — продавец;
  • moderator — модератор карточек;
  • dispute-operator — специалист по спорам;
  • finance — финансовый отдел.

Client-роль отвечает на вопрос «что этому человеку можно в конкретном приложении». В backoffice их видно лучше всего: card:approve (одобрить карточку), dispute:resolve (решить спор), payout:approve (подтвердить выплату). Один и тот же сотрудник может быть moderator на площадке и при этом иметь в backoffice только card:approve — модерировать карточки может, а трогать деньги нет.

Проверить, что деление верное, помогает простой вопрос: что произойдёт, если появится второе приложение. Роль seller останется прежней — продавец останется продавцом и в вебе, и в мобильном. А payout:approve в другом приложении означала бы уже другое действие — значит, её место в client-ролях.

Группы — чтобы не раздавать по одной. Сотрудников много, наборы прав типовые: группа «Модерация» несёт moderator + card:approve, группа «Споры» — dispute-operator + dispute:resolve. Новый сотрудник попадает в группу, и права приезжают вместе с ней. Продавцам и покупателям группы не нужны: у них роль одна и назначается при регистрации.

Что оказывается в токене. Покупатель, зашедший через storefront-web, получает access token, где realm_access.roles содержит buyer, а resource_access — пусто: в backoffice ему нечего делать. Модератор, зашедший через backoffice, несёт realm_access.roles: [moderator] и resource_access.backoffice.roles: [card:approve]. Order Service ходит по собственному токену, полученному без участия человека, и роли там лежат в тех же полях — только принадлежат они служебной учётной записи самого сервиса, а не пользователю.

Дальше это читают сервисы: Catalog пускает к модерации по card:approve, Order проверяет buyer на оформлении заказа. Но одной роли часто мало — продавец с ролью seller не должен править чужие карточки. Это уже проверка на конкретном объекте, и она разбирается в статье про роли и доступ.

Дополнительно: при первом чтении можно пропустить

Глубже: что выключить сразу после создания clientрасширенное

Свежесозданный client работает, и это плохой знак: несколько настроек по умолчанию или по привычке открывают дыры, которые в статьях упоминаются одной фразой. Чек-лист на пять минут после «Create client».

  • Direct access grants выключить. Это вход по паролю прямо через /token, без браузера и без второго фактора; нужен только для стенда и редких скриптов, а оставленный включённым он позволяет перебирать пароли мимо формы входа и обходить второй фактор.
  • Implicit flow выключить. Токен в адресной строке, в истории браузера и в логах прокси; поток объявлен устаревшим, о чём статья про authorization code flow.
  • Standard flow оставить, для серверных сервисов без пользователей включить только Service accounts и выключить всё остальное.
  • Valid redirect URIs точные, без * в конце домена и без http://localhost в боевом realm. Адрес с * позволяет отправить код входа на подконтрольный злоумышленнику путь; в новых версиях Keycloak предупреждает о *, но не запрещает.
  • Web origins явные, без *; значение + разрешает источники из списка redirect URIs, и это обычно то, что нужно для фронтенда.
  • Client authentication включить у серверных клиентов, и секрет хранить как секрет, а не в application.yml.
  • Full scope allowed выключить (вкладка «Client scopes», выделенный scope клиента, «Scope»). Пока включено, в токен попадают все роли пользователя во всех клиентах realm, и сервис оплаты видит роли из админки склада; выключенный режим кладёт только роли, явно назначенные этому клиенту, и заодно делает токен короче.
  • PKCE для public client обязателен: «Advanced», «Proof Key for Code Exchange Code Challenge Method» в S256.
  • Consent для своих приложений выключен, для сторонних включён: пользователь должен видеть, кому отдаёт доступ.
  • Срок жизни токенов у клиента не увеличивать сверх realm ради удобства: access в минуты, о причинах в статье про токены.

Проверить это можно не мышью, а экспортом клиента в JSON и сравнением с эталоном, о чём следующий раздел; в realm с десятком клиентов ошибка в одном находится только так.

Глубже: эксплуатация: realm как код, прокси, база и обновлениярасширенное

«Шестой сервис со своей базой» назван ценой Keycloak, и вот из чего эта цена состоит.

Realm как код. Всё, что настроено мышью, живёт в базе Keycloak и не переживёт «а давайте поднимем второй стенд». Realm экспортируют в файл: kc.sh export --dir /tmp/export --realm shop --users realm_file, и файл кладут в репозиторий; при старте контейнера каталог /opt/keycloak/data/import с флагом --import-realm создаёт realm, если его ещё нет. Для изменений в живом realm файл не годится (импорт не обновляет существующее), и тут берут keycloak-config-cli: он читает тот же JSON и приводит realm к нему, добавляя и меняя клиентов, роли и потоки, как миграции у базы. Альтернатива для тех, у кого инфраструктура в Terraform, это провайдер Keycloak. Секреты клиентов в файл не кладут: их подставляют переменными при применении.

За прокси. В проде Keycloak стоит за балансировщиком, который терминирует TLS, и ему нужно об этом сказать: KC_HOSTNAME=https://auth.example.com, KC_PROXY_HEADERS=xforwarded, KC_HTTP_ENABLED=true для внутреннего HTTP. Без этого адреса в iss и в ссылках на страницы входа собираются из внутреннего имени, и токены не проходят проверку издателя у сервисов. Консоль администратора наружу не выставляют: отдельный hostname для admin или доступ только из внутренней сети.

База и резервные копии. Пользователи, клиенты, роли и настройки лежат в PostgreSQL (KC_DB=postgres, строка подключения и учётная запись через переменные); сессии в новых версиях тоже хранятся в базе, а в старых жили в памяти кластера и терялись при перезапуске. Резервная копия Keycloak это резервная копия его базы плюс файл realm в репозитории; проверяют восстановлением на стенд, как любую другую, о чём говорит статья про репликацию.

Обновления. Мажорные версии выходят несколько раз в год, и переезжают по одной, не перепрыгивая; миграция схемы базы выполняется при старте новой версии сама и назад не откатывается, поэтому обновление начинают с копии базы. В заметках к выпуску читают раздел про удалённое: за последние версии исчезли старые адаптеры для Spring, изменились имена переменных администратора (KEYCLOAK_ADMIN стал KC_BOOTSTRAP_ADMIN_USERNAME) и режимы прокси.

Учётная запись администратора. Пользователь из переменных при первом старте это временный администратор в realm master; после первого входа заводят постоянного администратора с вторым фактором, а временного удаляют. В master не размещают приложения и пользователей продукта: он управляет остальными realm, и его компрометация это компрометация всего. Для автоматизации (config-cli, пайплайны) заводят service account с ролями manage-realm только на нужный realm, а не администратора master.

Коротко

  • Keycloak — отдельный сервер входа: хранит пользователей, проверяет пароли, выдаёт подписанные токены. Realm — изолированное пространство со своими пользователями, приложениями, ролями и группами; master только для администрирования, под проект создают свой.
  • Client — это запись о приложении, не о человеке. Public (секрета нет, защита через PKCE) — для фронтенда и мобильных; confidential (есть client secret) — для серверного бэкенда.
  • Права несёт не сам пользователь, а назначенные ему роли: realm-роль общая для всего realm (пропуск во всё здание), client-роль принадлежит одному приложению (ключ от комнаты), а группа раздаёт пачку ролей сразу многим.
  • В токен роли кладут протокол-мапперы (часто включены по умолчанию); realm-роли — в realm_access.roles, client-роли — в resource_access.<client-id>.roles. Роль есть, а в токене нет — проблема в мапперах или (для client-ролей) в области действия токена, но не в назначении.
  • Spring Boot как resource server настраивается одним issuer-uri: ключи он находит сам и проверяет токены локально, а роли Keycloak подхватывает JwtAuthenticationConverter с путём realm_access.roles и префиксом ROLE_. Минимум для связки: один realm, public и confidential client, realm-роли, пользователи.
  • В маркетплейсе это один realm, по client'у на приложение, realm-роли под «кто человек» и client-роли под «что ему можно». После создания client выключают Direct access grants, Implicit flow и Full scope allowed, задают точные redirect URIs и Web origins, PKCE S256 у public и секрет у серверных.
  • Эксплуатация: realm в JSON с --import-realm и keycloak-config-cli для изменений, за прокси KC_HOSTNAME и KC_PROXY_HEADERS, база PostgreSQL с копиями, мажорные версии по одной с копией базы, в master только администраторы с вторым фактором.
  • Valid redirect URIs и Web origins дают две первые ошибки подключения: несовпадение адреса — Invalid parameter: redirect_uri, отсутствие источника — ошибку CORS, которую читают как «токен не выдаётся».
  • Приложение без человека получает токен через service account: включить «Service accounts roles», роли вешать на запись service-account-<client>, остальные потоки выключить.
  • Составные роли раскрываются в токен целиком: токен растёт, а происхождение права по нему уже не восстановить; вкладывать глубже одного уровня и смешивать с группами не стоит.

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