05-identity-permissions.md 15 KB

路线 05:从登录追踪到租户隔离、权限检查与后台复验

学习场景

客户登录后查询自己的工单;同一身份提交 Agent 任务,工作进程在稍后执行时重新检查会话和权限;平台员工通过独立入口访问平台功能。沿三个场景观察凭据如何建立、身份如何验证、资源范围如何限制。

图中的后端路径均相对 src/zhifu/。连续箭头表示直接函数调用;标注“框架调用”的箭头表示 FastAPI 或 LangGraph 调用已注册函数。间断箭头表示 HTTP、持久数据或框架注册连接,箭头说明具体含义。数据节点使用类名,函数节点使用源码中的函数名。返回路线目录。

图 1:登录与一次工单查询

登录成功产生有效期为 3600 秒的 opaque Bearer token,数据库保存 token 的 SHA-256 摘要。客户查询工单时,服务端重新验证会话、租户与账户状态,然后检查店铺权限及工单客户归属。

flowchart TB
    CLIENT["客户端<br/>保存登录响应中的 token"]
    LOGIN["modules/identity/auth.py<br/>login<br/>限流、密码验证与会话创建"]
    SESSION[("modules/identity/access_models.py<br/>AccessSession<br/>token_hash、有效期与撤销状态")]
    GET["modules/after_sales/api.py<br/>get_ticket<br/>建立查询事务"]
    AUTH["modules/identity/auth.py<br/>authenticate<br/>读取 Bearer token 并计算摘要"]
    HASH["modules/identity/auth.py<br/>authenticate_hash<br/>复查会话、租户和账户"]
    TENANT[("modules/identity/models.py<br/>Tenant<br/>租户状态")]
    PRINCIPAL[("modules/identity/access_models.py<br/>Principal<br/>账户归属、类型与状态")]
    FIND["modules/after_sales/service.py<br/>AfterSalesService.find<br/>按 tenant_id 与资源 id 查询"]
    ACCESS["modules/after_sales/service.py<br/>AfterSalesService.access<br/>检查客户本人资源"]
    STORE["modules/identity/auth.py<br/>authorize_store<br/>检查店铺状态与指定权限"]
    GRANT[("modules/identity/access_models.py<br/>StoreGrant<br/>租户、账户、店铺、permission")]
    VIEW["modules/after_sales/service.py<br/>AfterSalesService.ticket_view<br/>返回授权范围内工单"]
    CLIENT -.->|HTTP POST /openapi/v1/auth/sessions;FastAPI 分发| LOGIN
    LOGIN -.->|事务保存 token 摘要| SESSION
    LOGIN -.->|HTTP 响应返回 token| CLIENT
    CLIENT -.->|"HTTP GET /openapi/v1/tickets/{ticket_id};FastAPI 分发"| GET
    GET --> AUTH
    AUTH --> HASH
    SESSION -.->|读取与重新刷新| HASH
    TENANT -.->|锁定并检查 ACTIVE| HASH
    PRINCIPAL -.->|锁定并检查 ACTIVE| HASH
    GET -->|认证成功后| FIND
    GET -->|取得工单后| ACCESS
    ACCESS --> STORE
    GRANT -.->|按四个字段读取权限记录| STORE
    GET -->|全部检查通过后| VIEW

login 按账户和客户端地址分别限制每 60 秒 10 次、60 次,失败请求也计数。密码验证通过后,它重新读取账户状态和密码摘要,再登记会话。authenticate_hash 对账户记录加锁并重新刷新会话状态,使当前操作与权限撤销具有明确的事务顺序。

find 将 auth.tenant_id 加入查询;authorize_store 检查店铺属于当前租户、处于 ACTIVE,且存在指定 StoreGrant;access 对 CUSTOMER 继续检查 subject_id == actor_id。无权访问业务资源时返回 RESOURCE_NOT_FOUND。身份或会话失效时返回 UNAUTHENTICATED。

图 1 源码导航

图 2:后台 Agent 运行重新验证身份

业务 API 在 queue_run 中保存请求身份的 token_hash。后台工作进程领取数据库中的任务,再调用编译后的 LangGraph。build_graph 注册节点,graph.ainvoke 触发节点执行;图中分别标记注册关系和框架调用关系。

flowchart TB
    API["modules/agent_runtime/api.py<br/>enqueue<br/>认证、店铺授权与幂等检查"]
    QUEUE["modules/agent_runtime/service.py<br/>queue_run<br/>保存身份与运行配置"]
    RUN[("modules/agent_runtime/models.py<br/>AgentRun<br/>身份摘要、状态与租约")]
    WORKER["modules/agent_runtime/worker.py<br/>execute_one<br/>领取并执行一次运行"]
    CLAIM["modules/agent_runtime/worker.py<br/>claim<br/>领取任务并增加 lease_token"]
    BUILD["modules/agent_runtime/runtime.py<br/>build_graph<br/>注册节点并 compile"]
    INVOKE["modules/agent_runtime/worker.py<br/>execute_one.invoke<br/>调用 graph.ainvoke"]
    LOAD["modules/agent_runtime/runtime.py<br/>build_graph.load<br/>读取持久 next_node"]
    MODEL["modules/agent_runtime/runtime.py<br/>build_graph.model<br/>模型请求前后复验"]
    LOCKED["modules/agent_runtime/runtime.py<br/>locked_run<br/>身份、控制权、租约与期限检查"]
    AUTH["modules/identity/auth.py<br/>authenticate_hash<br/>重新验证持久会话"]
    STORE["modules/identity/auth.py<br/>authorize_store<br/>重新检查 orders:read"]
    ACCESS[("modules/identity/access_models.py<br/>AccessSession / StoreGrant<br/>当前会话与权限记录")]
    API --> QUEUE
    QUEUE -.->|API 事务提交| RUN
    WORKER --> CLAIM
    RUN -.->|跨进程数据库领取| CLAIM
    WORKER -->|领取成功后| BUILD
    WORKER --> INVOKE
    BUILD -.->|注册 load 节点| LOAD
    BUILD -.->|注册 model 节点| MODEL
    INVOKE -->|LangGraph 框架调用 START 后的 load| LOAD
    INVOKE -->|LangGraph 框架调用;条件路由 next_node=model| MODEL
    LOAD --> LOCKED
    MODEL -->|模型请求前及保存响应前| LOCKED
    RUN -.->|读取身份摘要并锁定当前运行| LOCKED
    LOCKED --> AUTH
    LOCKED -->|核对运行身份、控制权和租约后| STORE
    ACCESS -.->|读取当前状态| AUTH
    ACCESS -.->|读取当前权限| STORE

图中选择 load 和 model 展示复验时机。工具子图的 execute 与 followup 节点也调用 locked_run。locked_run 检查 token 对应身份与 AgentRun 一致;有关联会话时检查 control_owner、control_version 和 active_run_id;随后确认 RUNNING 状态、有效租约、执行期限、图版本及店铺授权。

模型请求结束后先独立保存已经发生的用量,再复验身份与控制权并提交响应。身份失效等异常由 execute_one 记录到当前运行;它只在自己仍持有对应租约时修改运行状态。

图 2 源码导航

图 3:平台员工的独立身份

平台登录入口使用 PlatformAccount 和 PlatformSession。平台请求的 authenticate 在平台会话表中寻找凭据,并检查账户启用状态及 platform:manage。

flowchart TB
    HTTP["FastAPI<br/>/openapi/v1/platform/auth"]
    LOGIN["modules/platform/auth.py<br/>login<br/>平台账户密码验证与限流"]
    ACCOUNT[("modules/platform/models.py<br/>PlatformAccount<br/>平台账户与 permissions")]
    SESSION[("modules/platform/models.py<br/>PlatformSession<br/>平台 token 摘要与有效期")]
    ME["modules/platform/auth.py<br/>me<br/>查询当前平台身份"]
    LOGOUT["modules/platform/auth.py<br/>logout<br/>撤销当前平台会话"]
    AUTH["modules/platform/auth.py<br/>authenticate<br/>检查平台会话与 platform:manage"]
    HTTP -->|框架调用 POST /sessions| LOGIN
    HTTP -->|框架调用 GET /me| ME
    HTTP -->|框架调用 DELETE /sessions/current| LOGOUT
    ACCOUNT -.->|查询密码摘要并重新核对状态和权限| LOGIN
    LOGIN -.->|事务保存独立会话| SESSION
    ME --> AUTH
    LOGOUT --> AUTH
    SESSION -.->|读取并重新刷新| AUTH
    ACCOUNT -.->|锁定账户并校验权限| AUTH
    LOGOUT -.->|认证成功后设置 revoked=True| SESSION

平台登录成功后同样返回 3600 秒 token。authenticate 通过 PlatformSession.account_id 找到平台账户,锁定账户后再次刷新会话;随后检查当前 permissions 是否包含 platform:manage。me 返回 kind=PLATFORM,退出操作仅撤销本次平台会话。

图 3 源码导航

关键数据与状态

  • Principal.kind 包含 CUSTOMER、STAFF、SERVICE;客服、主管和商家管理员的具体职责通过店铺权限表达。可阅读 ROLE_PERMISSIONS 与 management_access 理解管理入口的身份检查。
  • StoreGrant 的四个主键字段共同限定权限。资源读取同时受 tenant_id、店铺权限和客户归属约束。
  • AccessSession 保存 token_hash、tenant_id、principal_id、expires_at、revoked。authenticate_hash 从当前记录构建 AuthContext。
  • 租户和账户需要处于 ACTIVE。有效 token 对应的账户或租户停用后,后续身份检查返回 401;业务资源授权不足时,相关入口返回 404。
  • AgentRun 保存原始身份摘要和执行控制信息。lease_token 与 lease_until 限制工作进程提交权;关联会话的控制版本限制会话接管后的继续执行。
  • PlatformAccount 和 PlatformSession 管理平台员工身份;平台权限通过 PlatformAccount.permissions 检查。

阅读检查问题

  1. 查询工单时,租户范围、店铺权限和客户本人归属分别在哪个函数检查?这些检查需要哪些字段?
  2. Agent 任务已经进入队列之后,客户退出登录或客服接管会话,哪些检查会阻止工作进程继续保存响应?模型用量为什么另行保存?
  3. 平台员工 token 在哪张表中验证?撤销平台会话与撤销客户会话分别修改什么记录?

相关测试阅读入口

本路线只阅读测试代码,未执行下列用例。