diff --git a/docs/API文档-鉴权与人员.md b/docs/API文档-鉴权与人员.md index 2d8d28e..95e9b58 100644 --- a/docs/API文档-鉴权与人员.md +++ b/docs/API文档-鉴权与人员.md @@ -9,7 +9,7 @@ | 项 | 值 | |---|---| | Base | 同源 `/api/*`(开发期 Vite 把 `/api` 反代到 `:8787`) | -| 认证 | `Authorization: Bearer `;RS256;`iss` = `https://keycloak.akhr-ai.com/realms/corp`;`aud` 须含本服务 clientId(`KEYCLOAK_CLIENT_ID`,建议 `api-gateway`) | +| 认证 | `Authorization: Bearer `;RS256;`iss` = `https://keycloak.akhr-ai.com/realms/corp`;`aud` 须含本服务 clientId —— **本部署是 `task-dispatcher`**(后端环境变量 `KEYCLOAK_CLIENT_ID`) | | 票据 | **GET** 且没带头时认 `?ticket=<60 秒票据>`(取自 `GET /api/auth/ticket`);其它方法不认 | | HEAD | 按 GET 判(框架把 HEAD 改写成 GET 再分发)—— 端归属、票据两处都与同路径的 GET 一模一样 | | 请求体 | JSON,`content-type: application/json` | @@ -367,24 +367,41 @@ type ApiError = { code: 'unauthenticated' | 'forbidden' | 'not-found' | 'validat ## 8. curl 实录 -用服务账号拿 token(自动化的正道,手册 §10;该 client 需 `serviceAccountsEnabled`、realm 角色 `admin`、且 audience mapper 指向本服务 clientId): +用服务账号拿 token(自动化的正道,手册 §10;该 client 需 `serviceAccountsEnabled`,且 audience mapper **指向本服务 clientId** —— +服务账号 token 默认不含自己的 clientId,漏了这条就会 401 并点名让你加 mapper): ```bash TOKEN=$(curl -s -X POST https://keycloak.akhr-ai.com/realms/corp/protocol/openid-connect/token \ - -d grant_type=client_credentials -d client_id=bpos-e2e -d client_secret="$KEYCLOAK_E2E_CLIENT_SECRET" | jq -r .access_token) + -d grant_type=client_credentials -d client_id=task-dispatcher -d client_secret="$KEYCLOAK_E2E_CLIENT_SECRET" | jq -r .access_token) -# 我是谁 +# 我是谁 —— 下面是 2026-08-26 的实测响应(服务账号首次调用即被 JIT 建档,落「未分配」岗) curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8787/api/me | jq . +# { +# "staff": { "id": "49a85fc7-020c-4098-ba38-af7b138129ea", +# "account": "service-account-task-dispatcher", +# "name": "service-account-task-dispatcher", +# "positionId": "3e36f2dd-982b-4412-bb1a-4c12a93c053d", +# "state": "active", "roles": ["service", "user"] }, +# "position": { "id": "3e36f2dd-…", "code": "NONE", "name": "未分配", +# "note": "未指派岗位;流程包不得声明它" }, +# "roles": ["service", "user"], +# "ends": ["workbench"] ← service 没配进「角色→端」表,只有 user 生效 +# } # 没带 token → 401 curl -s http://localhost:8787/api/me | jq . # { "code": "unauthenticated", "message": "缺少 Bearer token" } -# 端不够 → 403(拿一个只有 user 角色的 token 试) -curl -s -H "Authorization: Bearer $USER_TOKEN" http://localhost:8787/api/gov/tasks | jq . -# { "code": "forbidden", "message": "进入「运营中心」需要授权;你的角色(user)可进:工作台", "detail": { "need": ["ops"], "ends": ["workbench"] } } +# 端不够 → 403(实测响应) +curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8787/api/gov/tasks | jq . +# { "code": "forbidden", "message": "进入「运营中心」需要授权;你的角色(service, user)可进:工作台", "detail": { "need": ["ops"], "ends": ["workbench"] } } + +# aud 不对 → 401,话直接点到要改哪儿(实测响应:mapper 补上之前就是这个) +# { "code": "unauthenticated", "message": "token 的 aud 不含 \"task-dispatcher\" —— 去 Keycloak 给发 token 的那个 client 加 audience mapper" } # 票据 + SSE +# 实测:{ "ticket": "v1.NDlhODVmYzctMDIwYy00MDk4LWJhMzgtYWY3YjEzODEyOWVh.1787712844.x6HoZb7E…", +# "expiresAt": "2026-08-26T02:54:04.000Z" } ← 中段是 base64url 的 sub,末段是 HMAC TICKET=$(curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8787/api/auth/ticket | jq -r .ticket) curl -s -N "http://localhost:8787/api/stream?ticket=$TICKET" # event: ready @@ -406,4 +423,9 @@ curl -s -X POST http://localhost:8787/api/login | jq . ``` Task 12 已用契约(`packages/contracts/src/api/*.ts` + `org.ts`)核过本文:§6 总表逐行比对生成表、§7 字段名逐个核对 schema —— 全部一致,零改动。 -示例里的 UUID / 岗位 id / 时间戳仍按契约构造 —— 真实响应需要 Keycloak 侧前置 4(e2e / 试跑服务账号 client `bpos-e2e`:`serviceAccountsEnabled`、realm 角色 `admin`、挂本服务 audience mapper)先就位,这是管理员的活,本轮未就位;前置就位后用 §8 给的 curl 序列换成真实响应。 +**2026-08-26 补:已在真环境跑通,§8 的响应示例换成了实测响应。** 当时的部署事实: +`KEYCLOAK_CLIENT_ID=task-dispatcher`;`task-dispatcher` / `web-portal` / `web-console` 三个 client 都挂了指向 +`task-dispatcher` 的 audience mapper;realm 里 8 个用户,5 个持 `admin`(→ 三端)、3 个持 `user`(→ 工作台)。 +实测验过:验签 · claims 归一(噪声角色已滤)· JIT 建档 · 角色→端 · 端判定(工作台 200 / 运营中心 403)· +票据签发与 SSE 连通 · 票据在 POST 上被拒 · `HEAD` 与 `GET` 判定一致 · 退役路由 410。 +仍未实测的只有一件:**用真人 token 走一遍**(要前端接入,或临时开 Direct Access Grants,手册 §6.1)。 diff --git a/docs/前端对接说明-统一鉴权中心.md b/docs/前端对接说明-统一鉴权中心.md index c5b51b0..c76ef82 100644 --- a/docs/前端对接说明-统一鉴权中心.md +++ b/docs/前端对接说明-统一鉴权中心.md @@ -10,10 +10,13 @@ - 后端**不做登录**。前端(公司已在 Keycloak `corp` realm 建好两个前端 client:`web-portal` 用户端 `jcai-bpos.akhr-ai.com` / `web-console` 管理端 `jcai-bpos-opt.akhr-ai.com`,PKCE)自己拿 token。 -- token 的 `aud` 必须含本后端的 clientId(后端环境变量 `KEYCLOAK_CLIENT_ID`,建议 `api-gateway`); +- token 的 `aud` 必须含本后端的 clientId —— **本部署是 `task-dispatcher`**(后端环境变量 `KEYCLOAK_CLIENT_ID`); + `web-portal` 与 `web-console` 都已挂好指向它的 audience mapper(2026-08-26 实测确认),前端不用为此做任何事; 后端**不看 `azp`**,用哪个前端 client 取的 token 都行 —— 没有 aud 会全线 401,后端的 401 message 会点名让管理员加 audience mapper。 - 每个请求头带 `Authorization: Bearer …`。`GET /api/me` 回答两件事:我是谁、我能进哪几个端(`ends`)。 - **权限只到端**:`design`(设计中心 `/design`)/ `ops`(运营中心 `/ops`)/ `workbench`(工作台 `/`)。 + 当前 realm 里的角色不止 `admin` / `user` 两种(服务账号还持有 `service`),而**没配进「角色 → 端」表的角色一律不加端** —— + 所以 `ends: []` 不是假想的边界,是真会遇到的(§3 那一段说了怎么呈现)。 入口亮不亮、页面能不能进,只看 `ends`;进去之后哪个接口能调,后端按每条路由的归属自己判(§6 总表)。 - 两条带不了 Authorization 头的路(`EventSource` 与 ``)用 **60 秒票据**: `GET /api/auth/ticket` → 把 `ticket` 放到那条 GET 的 `?ticket=` 上。 @@ -24,7 +27,7 @@ ``` 浏览器(SPA,PKCE)──登录/刷新 token──▶ https://keycloak.akhr-ai.com/realms/corp │ - │ Authorization: Bearer (aud ∋ api-gateway) + │ Authorization: Bearer (aud ∋ task-dispatcher) ▼ apps/api 鉴权中间件(唯一一处) ① 路由归属:契约里每条路由自带 access(public / any / 端) @@ -183,7 +186,9 @@ async function openStream(onTouch: (t: Touch) => void, onOpen: () => void): Prom 可选 `KEYCLOAK_PROVISIONING_CLIENT_ID/_SECRET`(同步名册)、`JCAI_TEST_ACCOUNTS`(测试账号用户名)。 - Vite 的 `/api` 反代不变;浏览器直连 `keycloak.akhr-ai.com` 取 token,与后端无关(跨域由 Keycloak 的 Web Origins 管,手册有写)。 - 想在没前端的情况下打接口:用服务账号拿 token(见 API 文档 §8 的 curl),或让管理员临时给 `web-portal` 开 Direct Access Grants 用密码取一枚(手册 §6.1,用完关掉)。 -- 试跑 / e2e 用的「AI 账号」现在是 Keycloak 里的服务账号(`bpos-e2e`),工作台测试时用它的 token。 +- 试跑 / e2e 用的是 **`task-dispatcher` 的服务账号**(`service-account-task-dispatcher`)。 + 注意它当前只有 `service` + `user` 两个 realm 角色 → 只进得了工作台;要跑测试台(设计端)得在运营中心 + 给 `service` 配上 design,或给该账号加 `admin`。 ## 9. 接入完成的判据 @@ -201,5 +206,8 @@ async function openStream(onTouch: (t: Touch) => void, onOpen: () => void): Prom Task 12 已用契约把本文核过一遍:§2 的十个文件指针全部存在(`apps/web` 本轮未动,指针原样有效); §7 的退役路由与替代逐条对过 `docs/API文档-鉴权与人员.md` §6 的总表,一致。 -本文里出现的响应示例(`GET /api/me`、票据、403 detail)与 API 文档同源,同样按契约构造 —— -换真实响应要 Keycloak 侧前置 1 与 4(服务账号 client、audience mapper)先就位,本轮未就位,留给前置就位后去 API 文档 §8 换。 +**2026-08-26 补:整条链已在真环境跑通并实测。** Keycloak 侧前置已就位(`task-dispatcher` 及两个前端 client +的 audience mapper 齐备),后端以 `KEYCLOAK_CLIENT_ID=task-dispatcher` 启动、自检通过。实测验过的行为: +验签 → claims 归一 → 首次登录 JIT 建档(落 `NONE / 未分配` 岗)→ 角色→端 → 端判定;工作台 200、运营中心 403(带 `detail`); +票据签发后 `?ticket=` 真连上 SSE、同一张票据用在 POST 上被拒;`HEAD` 与 `GET` 判定一致。 +本文与 API 文档 §8 的响应示例现在都是**实测响应**,不再是按契约构造的样例。