--- schema: "hl-changelog/v2" ticket: "7234" title: "车务出行人明文与供应商域判权口径统一到 token 当前角色(契约不变,仅拒绝口径与审计语义)" consumer: "admin" author: "jw(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "not_required" frontend_owner: "mmg" frontend_ref: "" target_release: "" verified_at: "2026-09-07" status_note: "前端 mmg 已 grep 实查为 not_required:契约(请求/响应/错误码)全不变,仅后端判权依据由库默认角色改为 token 当前角色(X-Admin-Role),纯服务端加固。车务明文接口前端经 src/api/fleet/board.js:37 调用但契约不变;供应商 items/list 前端无该路径直接调用;前端 hasPermission(directives/permission.js、stores/user.js)是读自身 store 权限码做 UI 显隐,不感知后端判权口径;X-Admin-Role/roleHasPermission/PERMISSION_CHECK_FAILED 前端零读取。撤权 10 分钟生效为后端缓存 TTL,无需前端改动。" updated_at: "2026-09-07" base: "dev-v3" --- # 车务/供应商: 敏感读取判权统一到 token 当前角色 > **服务**: hl-fleet-service (8086/8186) · hl-resource-service · hl-order-service-v3 · hl-user-service > **PR**: #7272 > **Issue**: #7234 > **日期**: 2026-09-07 > **影响范围**: 管理后台「车务派单看板·查看出行人明文」与「供应商域读写」两条链路的鉴权判定口径 --- ## ⚠️ 关键变化 **请求、响应、错误码全部不变,前端不改代码。** 唯一变化是「谁有权限」的判定依据: 由**数据库默认角色**(`admin_user.current_role_id`)改为**本次请求 token 的当前角色**(网关注入的 `X-Admin-Role`)。 对正常账号无可见变化——正常登录时 token 角色与库角色一致。差异只在两者分叉时出现: 短信登录会把 token 角色临时置成 `CUSTOMIZER` 却不改库里的默认角色,改后这类会话一律按 `CUSTOMIZER` 判权,不再借用库角色的权限码。 **撤权最迟 10 分钟生效**:角色权限码走 Redis 缓存(`admin_permissions:v2:role:{roleKey}`,TTL 10 分钟)。撤掉某角色的权限码关联后,最多 10 分钟后该角色被拒。 --- ## 一、背景 出行人明文(姓名/证件号/手机号/紧急联系人)是平台敏感度最高的数据。链路上已有调用方白名单、token 与 adminId 一致性校验、二次权限校验、访问审计四道闸,但权限那道判的是**库角色**:一旦 token 角色与库角色分叉,闸门放行与否取决于一个与本次会话无关的字段。 这不是「当前公开入口已可越权读明文」——车务入口在网关层已被 `VEHICLE_MANAGER`/`SUPER_ADMIN` 门禁挡住,供应商守卫也先校验 token 角色集合。本单是**口径加固**:把「谁在操作」的唯一事实来源固定为 token 角色,消除库角色与 token 角色分叉时静默放权的可能,并补上 order-v3 内部二次校验缺失的角色绑定(不接受调用方自报角色)。与 #7210 复审返工同一口径。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|---|---|---|---|---| | 1 | 车务查看出行人明文 | POST | `/admin/fleet/board/orders/:orderId/travelers/plain` | 修改接口 | 契约不变;无权限判定由库角色改按 token 当前角色 | | 2 | 供应商列表(供应商域读写代表) | GET | `/admin/supplier/items/list` | 修改接口 | 契约不变;供应商域读/写/授信/状态/资源关系全部由库角色改按 token 当前角色,角色集合前置校验不变 | --- ## 三、接口详情 ### 1. 车务查看出行人明文 `POST /admin/fleet/board/orders/:orderId/travelers/plain` **VO**: `无请求体` → `Result>` #### 使用场景 车务派单看板「查看出行人明文」。持 `VEHICLE_MANAGER`/`SUPER_ADMIN` 且其角色拥有 `FLEET_TRAVELER_PLAIN_VIEW` 权限码的车务管理员,查看某订单出行人的明文姓名/证件号/手机号/紧急联系人。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | orderId | path | Long | 是 | 雪花 ID | 订单 ID | | Authorization | header | String | 是 | `Bearer <管理端 token>` | 网关透传 | #### 出参 `Result>` | 字段 | 类型 | 说明 | |---|---|---| | name | String | 明文姓名(不变) | | idNo | String | 明文证件号(不变) | | phone | String | 明文手机号(不变) | | emergencyContact | String | 紧急联系人(不变) | | emergencyPhone | String | 紧急联系人电话(不变) | | ageAtDeparture | Integer | 按订单出发日计算的年龄(不变) | 本单不改返回结构,字段与改前完全一致。 #### 请求示例 ```json POST /admin/fleet/board/orders/2094610801951203329/travelers/plain Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "success": true, "data": [ {"travelerId": "...", "name": "张三", "idNo": "...", "phone": "...", "emergencyContact": "...", "ageAtDeparture": 10} ] } ``` 成功时 `fleet_sensitive_access_log` 落一条 `SUCCESS`(`row_count` = 返回条数)。 #### 空数据 / 降级响应 该订单无出行人时返回空数组。权限服务不可用一律 fail-closed(不返回明文)。 #### 错误响应 ```json {"code": 605039, "success": false, "message": "无权查看出行人明文信息", "data": null} ``` | 码 | 触发 | 审计落库 | |---|---|---| | 403(网关) | 当前角色非 `VEHICLE_MANAGER`/`SUPER_ADMIN` | 请求未到达 fleet,**不写审计** | | 605039 | 到达 fleet 后:当前角色无 `FLEET_TRAVELER_PLAIN_VIEW` 权限码 | `DENIED` + `PERMISSION_DENIED` | | 605039 | 权限服务不可用(Feign 降级) | `DENIED` + `PERMISSION_CHECK_FAILED`(与上一行区分,供排查故障 vs 无权限) | #### 业务边界 - **判权口径变化**:无权限判定由 `hasPermission(adminId, FLEET_TRAVELER_PLAIN_VIEW)`(库角色)改为 `roleHasPermission(当前角色, FLEET_TRAVELER_PLAIN_VIEW)`(token 角色)。 - **审计语义不变**:拒绝仍写 `DENIED`+`PERMISSION_DENIED`;`FAILED` 仍只用于 order 服务调用失败分支。新增区分:权限服务降级写 `DENIED`+`PERMISSION_CHECK_FAILED`。 - **内部边界加固**:order-v3 侧二次校验改为从被校验的 token 上取角色判权,请求体声明的角色只做一致性比对,不一致即拒——不接受调用方自报角色。 --- ### 2. 供应商列表(供应商域读写代表) `GET /admin/supplier/items/list` **VO**: `SupplierListQueryReqVO` → `Result>` #### 使用场景 供应商管理列表查询。此处作为供应商域的代表:域内读/写/授信写/状态写/资源关系维护端点判权口径同步变化,请求/响应/错误码均不变。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | (查询条件) | query | — | 否 | 同改前 | 本单不改查询参数 | | Authorization | header | String | 是 | `Bearer <管理端 token>` | 网关透传 | #### 出参 `Result>` | 字段 | 类型 | 说明 | |---|---|---| | supplierId | String | 供应商 ID(雪花,序列化为字符串,不变) | | supplierNo | String | 供应商编号(不变) | | shortName | String | 供应商简称(不变) | 本单不改返回结构,字段与改前完全一致。 #### 请求示例 ```json GET /admin/supplier/items/list Authorization: Bearer ``` #### 响应示例 ```json {"code": 200, "success": true, "data": [ { "supplierId": "...", "shortName": "..." } ]} ``` #### 空数据 / 降级响应 无命中返回空数组。权限服务不可用一律按无权限 fail-closed(拒绝)。 #### 错误响应 ```json {"code": 403, "success": false, "message": "无权访问供应商数据", "data": null} ``` | 码 | 触发 | |---|---| | 403 | 当前 token 角色不在读集合(`READ_ROLES`=ADMIN/FINANCE/SUPER_ADMIN)内 | | 403 | 角色在集合内但该角色无对应权限码(读);写端点按其既有 `395002`/状态码拒 | #### 业务边界 - **判权口径变化**:域内私有判权由 `hasPermission(adminId, code)`(库角色)改为 `roleHasPermission(role, code)`(token 角色)。 - **角色集合前置校验不变**:`READ_ROLES`/`WRITE_ROLES`/`STATUS_WRITE_ROLES`/`RESOURCE_RELATION_*` 仍先按 token 角色集合挡一道,对正常账号无可见变化。 --- ## 四、契约约束与正确调用方式 ### ✅ 正确 / ❌ 错误理解 - ✅ 判权只认本次请求 token 的当前角色(`X-Admin-Role`)。多角色账号必须先切到有权限的角色再操作。 - ❌ 不要以为「库里默认角色有权限就一定能读」——短信登录等临时切角色的会话按 token 角色判,不借库角色的码。 - ✅ 撤销某角色的权限码后,最多 10 分钟生效(角色权限码缓存 TTL)。 --- ## 五、数据库行为 - 车务明文入口:只对 `fleet_sensitive_access_log` **追加**一条审计(成功 `SUCCESS`/拒绝 `DENIED`/失败 `FAILED`),无其他业务表写入。判权读的是 user-service 的角色权限码缓存(Redis,TTL 10 分钟),非本服务库表。 - 供应商域读端点:无写库;判权同样读 user-service 角色权限码。 - 全链路无表结构变更、无 Flyway。 --- ## 六、边界行为 - 车务明文入口:网关角色门禁(403,不写审计)在前,服务内权限码判定(605039,写审计)在后,口径都是 token 角色。 - 供应商域:角色集合前置校验(403)在前,权限码判定在后,口径都是 token 角色。 - 权限服务不可用:车务侧写 `DENIED`+`PERMISSION_CHECK_FAILED`(区分于无权限);供应商侧按无权限拒(其域无区分故障/无权限的审计语义)。 --- ## 六.6、修改前后对比 | 维度 | 改前 | 改后 | |---|---|---| | 车务明文判权依据 | 库默认角色 `admin_user.current_role_id`(`hasPermission(adminId, code)`) | 本次 token 当前角色 `X-Admin-Role`(`roleHasPermission(role, code)`) | | 供应商域判权依据 | 同上,按 adminId 落库角色 | 按 token 当前角色 | | order-v3 内部二次校验 | 只绑定 adminId,角色可由调用方自报 | 角色从被校验 token 上取,请求体声明只做一致性比对,不一致即拒 | | 权限服务不可用时车务审计 | `DENIED`+`PERMISSION_DENIED` | 新增区分:`DENIED`+`PERMISSION_CHECK_FAILED` | | 请求 / 响应 / 错误码 | — | **完全不变** | ## 六.7、影响评估 - **前端**:无需改代码。正常账号(token 角色=库角色)行为不变;只需知悉拒绝语义与「撤权最迟 10 分钟生效」。 - **正常账号**:无可见变化。 - **临时切角色会话**(如短信登录临时 `CUSTOMIZER`):按 token 角色判权,不再借用库角色权限码——这是本单的目的。 - **回滚**:纯判权口径与内部端点变更,无数据迁移,回滚 PR 即可;但需按部署顺序(user-service 先上)。 --- ## 七、不影响范围 - 请求参数、响应结构、错误码:全部不变。 - 网关路由与 `/admin/fleet/**` 角色门禁:不变。 - 脱敏返回结构、敏感访问审计落库链路:不变(只读,不改写入语义)。 - Flyway / 表结构:无变更。 - 服务间内部端点(`/internal/user/admin/access-token-context` 新增替代 `validate-access-token`)属服务间调用,不面向前端。 --- ## 八、测试环境已验证 TEST 网关 + 真实鉴权实测(2026-09-07,PR #7272 分支部署 user→order-v3→fleet→resource): ### 车务明文(AC-10/AC-11) - 临时 `CUSTOMIZER` token 调明文入口 → 网关 `403`,`fleet_sensitive_access_log` 计数不变(未到 fleet)。 - 真实 `VEHICLE_MANAGER`(持 `FLEET_TRAVELER_PLAIN_VIEW`)登录 token 调同入口 → 返回明文(3 条),审计落 `SUCCESS`(row_count=3)。 - 撤掉 `VEHICLE_MANAGER` 的 `FLEET_TRAVELER_PLAIN_VIEW` 关联并等角色缓存过期后 → 同账号同角色被拒 `605039`,审计落 `DENIED`+`PERMISSION_DENIED`;恢复关联后放行。 ### 供应商域(AC-12) - 同一 adminId(库角色 `VEHICLE_MANAGER`,无 `supplier:list`),token 角色 `ADMIN`/`FINANCE` → 200 出数,`CUSTOMIZER`/`VEHICLE_MANAGER` → 403。判权按 token 角色而非库角色(否则四种都应被拒)。 ### 单测与门禁 - user/order-v3/fleet/resource 本单新增改动单测全绿;fleet `spotless:check` 干净。含 AC-4 自报角色绕过用例、AC-5 实经 fallback 工厂用例。 --- ## 十、相关文档 - 工单 #7234;同口径前序 #7210 复审返工。 --- ## 关联 / 联系人 ### 链接 - Issue: https://git.1814.love:8443/wx/HL/issues/7234 - PR: https://git.1814.love:8443/wx/HL/pulls/7272 ### 联系人 - 后端: jw - 前端: mmg(无需改代码;知悉拒绝语义与撤权 10 分钟生效即可)