This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/docs/adr/ADR-012-keycloak-identity.md
T
xcsandClaude Opus 5 bb22545bdf 路由表字面重复也掀桌子;死信没收件人时出声;票据横向范围与「权限只到端」写进 ADR
**I1:字面路由重复注册仍是静默覆盖 —— 与参数侧的掀桌子不对称。**
两条**字面**契约同方法同路径撞上时,后写覆盖先写,命中谁全看 `Object.values(contracts)`
的顺序。参数模版撞了会抛(理由是「命中谁只看声明顺序 = 静默判错权限」),而字面这一半
没设防:同一个病、同一个后果。把守卫从 59 处收成 1 处之后,注册表是**唯一**的授权
事实源,它的自检不该有半边缺口。`size` 一并从 `routes.length` 改成
`literal.size + patterns.length`(重复被拒之后两者相等,但语义上它说的是「表里有几条」)。
`lookup` 上补一句:它只认 HTTP_METHODS 那五个,**归一是调用方的职责**;测里钉了
`lookup('HEAD', …)` 仍是 undefined —— 将来有人把中间件那行归一挪走,这条分工还在。

**顺手两条:**
- 死信通知的收件人为空时不出声(`deadLetters.report`),而 container 的 governanceStaff
  对同一件事会往 stderr 喊一句 —— 后者的理由(把「出事了」无声丢掉最糟)在前者同样成立。
- ADR-010 状态行补「已被 ADR-012 取代」:ADR-012 说了取代,但先翻到 ADR-010 的人看不到。

**两条不修,写进文档:**
- 票据的**横向范围**进 ADR-012 与 debt:票据里没有用途,60 秒内它等于持票人的
  **全部读权限** —— 凭一枚为下载附件取的票据,同一分钟里读得了整张客户主数据。
  不是提权(那些读权限他本来就有),风险在留痕面(URL 走浏览器历史 / 反代日志 / Referer)。
  收敛做法记进 debt:签发时把用途绑进票据(scope ∈ {stream, file}),或中间件对认票据的
  路径设白名单。
- 「权限只到端」的影响面进 ADR-012 的「后果」:design 现在同时意味着装活动、跑测试台、
  传文件、探大模型;design|ops 意味着 `GET /api/gov/data/:domain/:object` 能翻整张客户
  主数据(**从前要 data:read 才行**)。半年后有人问「为什么设计的人看得到客户手机号」,
  ADR 里得有这句话在。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 19:42:48 +08:00

4.9 KiB

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;治理路由里 59 处逐条守卫整体删除(governance-ops 45 + governance 14);覆盖测试 fail-closed。
  5. 名册:岗位仍是本系统的表(哨兵岗 NONE / 未分配,运营中心指派);名册经 Admin API 同步 (master realm 的 provisioning 服务账号,只要 view-users)+ 首登即建。state 只由同步置 disabled(Keycloak 停用或删除),有效 token 不改它 —— 撤权在同步之后即时 403。
  6. 票据:EventSource 与 <a href download> 带不了 Authorization 头 —— GET /api/auth/ticket 换一枚 60 秒 HMAC 票据(HKDF(JCAI_SECRET_KEY, 'jcai-ticket')),只在 GET 的 ?ticket= 上认; 票据里没有权限,中间件拿 sub 回库现算。无状态、不单次:60 秒重放窗换零状态表。 横向范围要说明白:票据里既然没有用途,它在这 60 秒里就等于持票人的全部读权限 —— 凭一枚为下载附件取的票据,同一分钟内照样读得了 GET /api/gov/data/:domain/:object(整张客户主数据)。 这不是提权(该用户本就有这些读权限),风险在留痕面:票据走在 URL 上, 浏览器历史、反向代理日志、Referer 都可能把它留下来。收敛做法见 docs/debt.md 鉴权段 (签发时把用途绑进票据,或中间件对认票据的路径设白名单)。
  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 寿命);停用经同步后即时。
  • 权限只到端,粒度就到此为止 —— 这是有意的收敛,但半年后有人问起时,答案得在这儿写着: design 现在同时意味着装活动、跑测试台、传文件、探大模型;design|ops 意味着 GET /api/gov/data/:domain/:object 能翻整张客户主数据(从前要 data:read 这类客体授权才行)。 产品要的就是「不同角色能进哪几个端」,再细的粒度没有需求撑着;真要收窄, 该走的是「给那几条路由单列一个端」,不是把客体 × 动作那套授权请回来。
  • web 需按 docs/前端对接说明-统一鉴权中心.md 改造(登录 / 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「鉴权」段。