Files
BPOS-Service-Gateway/docs/keycloak-setup-log.md
T
lovefyfandClaude Opus 5 ec0293fb79 docs: Keycloak 配置补齐——service 角色/audience mapper 落地与 token 验证
人类补授 manage-realm + manage-clients 后重跑第 1 轮被 403 的操作:
① realm 角色 service 已建(201);② 已挂到 task-dispatcher / api-gateway
两个服务账号(204,composite 复核含 service);③ task-dispatcher 加
aud-data-service + aud-agent-service(201×2);④ api-gateway 加
aud-task-dispatcher(201);⑤ web-portal 早已存在,全程零写操作。

三项验证解码真实 token 全部 PASS,Task 11 的人类管理员路径与服务间
调用路径前置条件均已满足。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 07:08:06 +08:00

13 KiB
Raw Blame History

Keycloak 配置与验证记录(N1 Task 10)

目标 realm:corp @ https://keycloak.akhr-ai.com 依据:docs/keycloak-agent-guide.md §6(admin token)、§7.4/§7.5(角色与 mapper)、§10(验证) 最终结论:DONE —— ①②③④ 全部落地,⑤ 早已存在无需变更;三项 token 验证全部 PASS。 本记录不含任何 secret、密码或完整 token,只记录操作与解码后的 aud / realm_access.roles claim。

本任务分两轮执行:

轮次 时间 结果
第 1 轮 2026-08-13 BLOCKED —— provisioning 服务账号缺 manage-realm / manage-clients,①③④ 被 403,② 因 ① 未完成而阻塞
第 2 轮 2026-08-13(人类补授权限后) DONE —— ①②③④ 全部成功,验证全绿

0. 执行凭证与权限变化

按指南 §6.2 用 master realm 的 provisioning 服务账号(client_credentials)取 admin token。 解码该 token 得到它在 corp-realm 管理 client 上的实际角色:

第 1 轮(阻塞时)

["manage-users", "view-users", "view-clients", "query-clients", "query-groups", "query-users"]

对照指南 §6.3,缺 manage-realm(建 realm 角色)与 manage-clients(加 mapper)。 按安全规则未做任何提权尝试,只记录并上报,由人类在服务器侧补授。

第 2 轮(人类执行 kcadm add-roles --rolename manage-realm --rolename manage-clients 后)

["manage-realm", "manage-users", "view-users", "view-clients",
 "manage-clients", "query-clients", "query-groups", "query-users"]

两个缺失角色已到位,且未授予 realm-admin,符合指南 §12-5 最小权限原则。

对象清单

对象 UUID
client task-dispatcher 3e7c17c6-829e-43d1-95e5-aad5d8ae750e
client api-gateway e02d6fc4-d463-49d8-bc24-4e85dde551d3
client web-portal 0e77f3a9-9a0c-4975-b441-14c183d12665
client data-service feddbc60-4cc6-4e49-a1d8-a969fc4ea46f(未改动)
client agent-service b53cb9f3-3bad-4a10-bb9c-f2df8982e2ca(未改动,归智能体方)
SA service-account-task-dispatcher 49a85fc7-020c-4098-ba38-af7b138129ea
SA service-account-api-gateway e3f07791-7494-496a-953e-87f8bbc33fb5
用户 chaihao 96d21014-4e27-431e-a845-8d467bbad287
realm 角色 service(本次新建) f25993d7-6707-4ee2-9a6b-70b9511f4a79

1. 逐项操作结果

① 建 realm 角色 service —— ✅ created

  • 第 1 轮:POST /roles → 403(缺 manage-realm);连 GET /roles 都 403。已跳过并上报。
  • 第 2 轮先 GET:GET /roles?briefRepresentation=true&max=200 → http=200, 返回 admin user offline_access default-roles-corp uma_authorization —— 确认 service 不存在,非重复创建。
  • 写入:POST /admin/realms/corp/roles,body {"name":"service","description":"服务间调用面角色"} → HTTP 201
  • 复核:GET /roles/service → http=200
{ "id": "f25993d7-6707-4ee2-9a6b-70b9511f4a79", "name": "service",
  "description": "服务间调用面角色", "composite": false, "clientRole": false }

该角色仅授予服务账号,用于按指南 §5.7-3 的建议「判角色而不是判 preferred_username 前缀」区分服务调用。

② 把 service 挂到两个服务账号 —— ✅ created(两个都成功)

  • 第 1 轮:因角色不存在无法取得 role id 而阻塞。当时用一个不存在的 role id 做了不产生变更的权限探针, 返回 404 Role not found 而非 403,据此判定 manage-users 已足够、唯一阻塞点是 ①。第 2 轮证实了这个判断。
  • 第 2 轮先查 GET /users/{uid}/role-mappings/realm/composite:两个服务账号均不含 service,确需授予。
  • 写入 POST /users/{uid}/role-mappings/realm,body 为角色对象数组(指南 §7.4): [{"id":"f25993d7-6707-4ee2-9a6b-70b9511f4a79","name":"service"}]
服务账号 HTTP composite AFTER
service-account-task-dispatcher 204 ["user","offline_access","service","default-roles-corp","uma_authorization"]
service-account-api-gateway 204 ["user","offline_access","service","default-roles-corp","uma_authorization"]

③ 给 task-dispatcher 加两个 audience mapper —— ✅ created(两条都成功)

  • 第 1 轮:两条均 403(缺 manage-clients)。
  • 第 2 轮先 GET GET /clients/{uuid}/protocol-mappers/models → http=200,列表为空,无同名 mapper,非重复创建。
  • 写入(body 按指南 §7.5,protocolMapper=oidc-audience-mapper,access.token.claim=true):
mapper 名 included.client.audience HTTP
aud-data-service data-service 201
aud-agent-service agent-service 201

④ 给 api-gateway 加 aud-task-dispatcher —— ✅ created

  • 第 1 轮:403(缺 manage-clients)。
  • 第 2 轮先 GET → http=200,列表为空,非重复创建。
  • 写入 aud-task-dispatcher(included.client.audience=task-dispatcher)→ HTTP 201。

⑤ web-portal 加 aud-data-service(D7 选项 a)—— ✅ already-present,全程零写操作

前置确认(要求:不得给人类授角色,只做校验) GET /users?username=chaihao&exact=true → id;GET /users/{id}/role-mappings/realm/composite → http=200:

admin, user, offline_access, default-roles-corp, uma_authorization

chaihao 确实已持有 realm 角色 admin,无需也未授予任何角色。

GET /clients/{web-portal-uuid}/protocol-mappers/models → http=200,目标 mapper 已存在,按幂等原则未创建重复项。

变更后的 mapper 全景(复核 GET .../protocol-mappers/models)

client mapper 类型 included.client.audience access.token.claim
task-dispatcher aud-data-service oidc-audience-mapper data-service true
task-dispatcher aud-agent-service oidc-audience-mapper agent-service true
api-gateway aud-task-dispatcher oidc-audience-mapper task-dispatcher true
web-portal aud-api-gateway oidc-audience-mapper api-gateway true
web-portal aud-agent-service oidc-audience-mapper agent-service true
web-portal aud-task-dispatcher oidc-audience-mapper task-dispatcher true
web-portal aud-data-service oidc-audience-mapper data-service true
web-portal groups oidc-group-membership-mapper — true

(web-portal 5 条全部为既有配置,本次未增删改。)

共享 client 影响面说明(SHARED-CLIENT blast radius)

  • 「web-portal 用户 token 的 aud 含 data-service」这一影响面在本任务之前就已存在, 与指南 §1 末尾「当前的受众模式:网关透传,web-portal 上挂了 4 个 audience mapper」完全一致。 本次任务没有扩大它,属于既有的、已文档化的架构决策。
  • 影响面本身仍然成立并在此记录备查:每一个经 web-portal 登录的用户,其 token 的 aud 都包含 data-service(以及另外 3 个后端)。代价是任一持有该 token 的下游服务技术上也能调其它服务。
  • 收紧方式(指南 §1 / §8.3):把对应 audience mapper 从 web-portal 摘掉,改由 api-gateway 用 token exchange 单独换发。这是局部改动,不需要重构。
  • 新增影响面(本次引入,需知悉):task-dispatcher 的服务账号 token 现在同时含 data-service 与 agent-service 两个受众,因此该 token 技术上可调用这两个服务中的任何一个; api-gateway 的服务账号 token 含 task-dispatcher。这是服务间调用面的预期设计(Task 10 目标), 但同样意味着这两个 secret 的泄漏面对应上述受众范围,须按指南 §12-4 妥善保管。

2. 验证(指南 §10,解码真实 token,不看 2xx)

两个服务的 client secret 均于运行时从各自 .env 读取(BPOS-Service-Task/.env、BPOS-Service-Gateway/.env), 未落盘、未复制到任何位置、未出现在本文件中。

V1 task-dispatcher client_credentials —— ✅ PASS

{
  "azp": "task-dispatcher",
  "preferred_username": "service-account-task-dispatcher",
  "aud": ["agent-service", "data-service", "account"],
  "realm_access": { "roles": ["default-roles-corp", "service", "offline_access", "uma_authorization", "user"] }
}
  • aud 含 data-service ✅ 且含 agent-service ✅
  • realm_access.roles 含 service ✅

(第 1 轮同一请求为 aud: "account"、roles 无 service,即 FAIL。)

V2 api-gateway client_credentials —— ✅ PASS

{
  "azp": "api-gateway",
  "preferred_username": "service-account-api-gateway",
  "aud": ["task-dispatcher", "account"],
  "realm_access": { "roles": ["default-roles-corp", "service", "offline_access", "uma_authorization", "user"] }
}
  • aud 含 task-dispatcher ✅
  • realm_access.roles 含 service ✅

(第 1 轮同一请求为 aud: "account"、roles 无 service,即 FAIL。)

V3 chaihao 经 web-portal —— ✅ PASS(两轮结果一致,未受本次变更影响)

密码模式不可用:grant_type=password + client_id=web-portal 返回 {"error":"unauthorized_client","error_description":"Client not allowed for direct access grants"}。 查 client 配置确认这是正确现状,不是故障:

{ "clientId": "web-portal", "publicClient": true,
  "standardFlowEnabled": true, "directAccessGrantsEnabled": false }

与指南 §1「两个前端只开 standard flow」、§12-3「不要用密码模式做生产登录」一致。 故改用指南 §7.5 / §10 认可的预览端点 GET /clients/{web-portal-uuid}/evaluate-scopes/generate-example-access-token?scope=openid&userId={chaihao}(http=200):

{
  "azp": "web-portal",
  "preferred_username": "chaihao",
  "aud": ["agent-service", "api-gateway", "data-service", "task-dispatcher", "account"],
  "realm_access": { "roles": ["default-roles-corp", "offline_access", "admin", "uma_authorization", "user"] }
}
  • aud 含 data-service ✅
  • realm_access.roles 含 admin ✅
  • 复核:chaihao 不含 service 角色 ✅(service 只挂服务账号,未污染人类用户)
  • ⚠️ 说明:这是同一套 mapper 管线生成的预览 token,不是浏览器真实登录换来的 token。 真实 token 需走授权码+PKCE(指南 §8.1)。Task 11 若要用真 token,需从前端登录后取, 或由人类在浏览器登录后复制 token。

3. Task 11 前置条件

路径 状态 依据
人类管理员(chaihao / web-portal) ✅ 满足 V3:aud 含 data-service,roles 含 admin → 可通过 requireRealmRole('admin') + aud=data-service 的建域接口
服务间调用(task-dispatcher → data/agent) ✅ 满足 V1:aud 含两个目标,roles 含 service
服务间调用(api-gateway → task-dispatcher) ✅ 满足 V2:aud 含 task-dispatcher,roles 含 service

两条前置路径均已满足,Task 11 可按设计执行。


4. 交接与注意事项

  • 智能体方(agent-service):agent-service 自身的 audience 配置归智能体团队,本次仅知会、未触碰 (UUID b53cb9f3-3bad-4a10-bb9c-f2df8982e2ca)。 现在 task-dispatcher 的服务账号 token 已带 aud=agent-service,智能体方需确认自己按指南 §5.3 校验 aud 含 agent-service,并按 §5.7 处理服务账号 token 的差异 (无 groups、sub 查不到业务用户、用 realm_access.roles 含 service 判定服务调用)。
  • data-service 未配 secret 是正确的(指南 §5.2 / §12-4,只验不调),本次未给它加任何凭据。
  • 各下游服务的角色判断:现在可以用 requireRealmRole('service') 区分服务间调用, 不要再判 preferred_username 的 service-account- 前缀(指南 §5.7-3)。
  • token 生效延迟:access token 有效期 300 秒,旧 token 在过期前仍不含新 claim, 排查时先确认手上的 token 是变更后重新签发的。

5. 附:人类侧已执行的补授命令(记录备查)

cd /opt/keycloak
PROV=$(sudo ./kc kcadm get clients -r master -q clientId=provisioning \
       --fields id --format csv --noquotes | tail -1)
SA=$(sudo ./kc kcadm get "clients/$PROV/service-account-user" -r master \
     --fields id --format csv --noquotes | tail -1)
sudo ./kc kcadm add-roles -r master --uid "$SA" \
  --cclientid corp-realm --rolename manage-realm --rolename manage-clients

补授后 provisioning 账号具备 manage-realm + manage-clients,但仍未授予 realm-admin, 符合指南 §12-5。若后续不再需要这两个角色,可用 remove-roles 收回。