# ADR-012 统一鉴权中心(Keycloak):身份、名册与「角色 → 端」 状态:已接受(2026-08-25) 背景出处:`docs/keycloak-backend-guide.md`(Keycloak 26.7.1 / jose,核实日期 2026-08-12); 设计稿 `docs/superpowers/specs/2026-08-25-keycloak-auth-design.md`。 取代:ADR-010(口令 + 服务端会话)与编码规范 §4 原「不引 OAuth/OIDC」的定调。 ## 问题 公司有了统一鉴权中心,本系统还在自建账号口令、服务端会话表与岗位授权(客体三类 × 动作三种)。 人要在两处维护,权限模型也比产品实际需要的细太多 —— 用户只要「不同角色可以动态配置能进哪几个端」。 ## 决策 1. **资源服务器,不做 BFF**:前端(`web-portal` / `web-console`,PKCE)自取 token;后端只本地验签 Bearer JWT(`jose`:RS256 写死、校 iss 与 aud、30s 容差、远程 JWKS 缓存 10 分钟 / 冷却 30 秒), 不回源、不 introspect、**不配 client secret**。 2. **身份在 token 里**:`Staff.id` = `sub`;`account` = `preferred_username` 只作显示; 首次见到即建档(JIT),账号 / 姓名 / 邮箱 / 角色变了才写,`last_seen_at` 60 秒内不重写。 3. **权限只到端**:三个端 design / ops / workbench;`sys.role_access` 一行一个 realm 角色, 运营中心可改;一个人的端 = 其角色命中行的并集。缺省 `admin → 三端`,`user → workbench`。 **锁死不变量**:改完至少一名在册活跃员工仍能进运营中心,否则 409。 4. **路由归属进契约**:`RouteContract.access` 必填(public / any / 端 / 端数组);app 层一处中间件 按注册表判 401 / 403;治理路由里约 100 处逐条守卫整体删除;覆盖测试 fail-closed。 5. **名册**:岗位仍是本系统的表(哨兵岗 `NONE / 未分配`,运营中心指派);名册经 Admin API 同步 (master realm 的 provisioning 服务账号,只要 `view-users`)+ 首登即建。`state` 只由同步置 `disabled`(Keycloak 停用或删除),有效 token 不改它 —— 撤权在同步之后即时 403。 6. **票据**:`EventSource` 与 `` 带不了 Authorization 头 —— `GET /api/auth/ticket` 换一枚 60 秒 HMAC 票据(`HKDF(JCAI_SECRET_KEY, 'jcai-ticket')`),只在 GET 的 `?ticket=` 上认; 票据里没有权限,中间件拿 sub 回库现算。无状态、不单次:60 秒重放窗换零状态表。 7. **退役**:`login` / `logout` / `session` / 建人 / 停用 / 岗位授权八条路由回 410(新错误码 `gone`), 契约标 `@deprecated` 留一轮(§12.4 三步);`sys.session` `sys.position_grant` `staff.password_hash` drop。 8. **测试账号**:`JCAI_TEST_ACCOUNTS`(Keycloak 用户名)命中者落 TEST 岗 + 独立工作组(ADR-011 原语义); e2e 与试跑用服务账号(client_credentials),不用人的账号。 ## 后果 - 撤角色的时效 ≤ 5 分钟(token 寿命);停用经同步后即时。 - web 需按《前端接入手册》改造(登录 / Bearer / 401 跳转 / SSE 与下载改走票据 / 入口亮灯改读 `/api/me` 的 `ends`), 改造前登不进 —— 这是「只做后端」的既定后果。 - **部署顺序是先跑迁移、再 `reset-runs`**:迁移 0032 清空 `sys.staff` 并连带清掉两张持有身份的 **配置**表(`sys.node_owner`、`sys.trigger` 的确认位);而 `sys.task.owner_id` 这类**无外键**的 运行数据列不受迁移约束,若顺序颠倒,这些运行数据会指着一个已经不存在的身份变成孤儿 —— `reset-runs` 必须排在迁移之后跑,才能把这批指着旧身份的运行数据一并清掉。 - Keycloak 侧前置(管理员):本服务 client(建议复用 `api-gateway`)、`web-console` 补 audience mapper、 provisioning 服务账号、e2e 服务账号 client(`bpos-e2e`,realm 角色 admin,firstName「AI」)。 - 欠账见 `docs/debt.md`「鉴权」段。