主题
请求生命周期与服务拓扑
本章目标:
- 把一个请求从浏览器到后端/引擎的每一跳讲清楚:谁鉴权、谁路由、谁注入身份头
- 看懂三类典型请求(页面访问、业务 API、Agent SSE)各走什么路径
- 理解 forward_auth 这道"集中鉴权前置"如何让所有上游服务无需各自实现登录
TL;DR
a-cdm 所有外部请求先过 Caddy。Caddy 对受保护路径做 forward_auth → acdm-backend:8002/auth/verify:verify 用 acdm_session cookie 解 JWT 查库,成功返 200 + X-Auth-User-Id/Email/Name/Role 四个头,Caddy 把这些头复制给上游再放行,失败原样返 401。前端 /workspace/* 由 proxy.ts 守卫:无 cookie 直接 302 到 /api/acdm/auth/login 触发 Keycloak 登录。dev 不起 Caddy,由 proxy.ts 自己解 cookie 注入 X-Auth-* 头顶替 forward_auth,后端校验逻辑两边完全一致。
Overview(为什么用"集中 forward_auth"而不是每个服务各自鉴权)
a-cdm 有四个对外服务(前端、控制面、引擎 Gateway、引擎 Runtime、ERP)。如果每个服务各自实现"解析 cookie/token、查用户、判权限",会有三个问题:引擎是 vendored 代码不该塞 Keycloak 逻辑;鉴权实现散落多处难以保证一致;改鉴权要改 N 个服务。
a-cdm 的解法是 forward_auth 集中前置:Caddy 在把请求转给任何受保护上游之前,先同步调一次 acdm-backend 的 /auth/verify。verify 是唯一懂 Keycloak/JWT/用户表的地方,它把"你是谁"压缩成四个 X-Auth-* HTTP 头。上游服务(引擎、ERP)只需"信任并读取这些头",完全不碰鉴权逻辑。这就是为什么引擎里 acdm_auth.py 只做"从 header 提取身份"而不做"验证密码"——验证那步已经被 Caddy+verify 做完了。
Architecture:四类参与者
| 参与者 | 职责 | 关键文件 | Source |
|---|---|---|---|
前端 proxy.ts | /workspace/* 未登录守卫 + dev/`/api/langgraph | memory` header 注入 | deer-flow/frontend/src/proxy.ts |
| Caddy 网关 | 路径反代 + forward_auth 鉴权前置 | deploy/prod/Caddyfile.keycloak-v1 | openspec/specs/architecture/spec.md:104-115 |
/auth/verify | 解 cookie 查库,产出 X-Auth-* 四头 | acdm-backend/app/auth/verify.py | acdm-backend/app/auth/verify.py:16-27 |
| 上游服务 | 信任并消费 X-Auth-* 头 | acdm-backend / 引擎 / ERP | — |
/auth/verify 实现极简(acdm-backend/app/auth/verify.py:16-27):Depends(get_current_user) 复用 cookie 解析 + DB 查询,成功就返 200 + 四个头:X-Auth-User-Id(user.id)、X-Auth-User-Email、X-Auth-User-Name、X-Auth-User-Role("admin" 或 "user")。注释 verify.py:4 说明:401 时 Caddy 原封不动把 401 返客户端,前端 JS 跳 /auth/login。
Data Flow:三类典型请求
请求 1:首次访问页面(未登录)
proxy.ts:82-89:PROTECTED_PATHS = ["/workspace"],无 acdm_session cookie → NextResponse.redirect("/api/acdm/auth/login")。后端接力到 Keycloak(详见鉴权章)。
请求 2:业务 API(已登录)
- 浏览器
GET /api/acdm/projects,带acdm_sessioncookie。 - Caddy 命中
@acdm_apimatcher,先forward_auth → acdm-backend:8002/auth/verify。 - verify 解 cookie 查库,返 200 +
X-Auth-*四头。 - Caddy
copy_headers把四头注入原请求,strip/api/acdm前缀,反代acdm-backend:8002。 - acdm-backend 业务路由处理,
get_current_user/ 三级授权依赖再次校验(详见授权章)。
注意 /api/acdm/auth/* 不走 forward_auth(openspec/specs/architecture/spec.md:108)——登录入口本身不能要鉴权,否则死循环。
一个请求在鉴权维度上的状态机:
请求 3:Agent 流式对话(SSE)
- 前端发起
/api/langgraph/threads/{id}/runs/stream。 - 生产:Caddy forward_auth → verify → 注入
X-Auth-*→ strip/api/langgraph→ 反代deer-flow-langgraph:2024(Aegra)。 - dev:不起 Caddy,
proxy.ts:77-78命中AUTH_INJECT_PREFIXES = ["/api/langgraph","/api/memory"],injectAuthHeaders()从 cookie 解 JWT payload(parseJwtPayloadbase64url 解码,不验签,proxy.ts:40-55),注入X-Auth-User-Id/X-Auth-User-Role,Next.js rewrite 直连 2024。 - 引擎
acdm_auth.authenticate(headers)从X-Auth-User-Id提取身份,做 owner_sub 隔离。 - Aegra 流式产出,SSE 持续推回前端;断线可 reattach(详见 AI 消息流章)。
关键不变量(proxy.ts:24):生产 dev 同路径,无后门。dev 的 proxy.ts 只是搬运身份头顶替 Caddy 这一跳,后端 acdm_auth 校验逻辑两边一字不差。
Implementation:proxy.ts 三职责
deer-flow/frontend/src/proxy.ts 是 Next.js 16 的请求前置(从 middleware.ts 重命名,导出函数名必须是 proxy,proxy.ts:1-4),config.matcher(proxy.ts:92-99)只拦 /workspace/*、/api/langgraph/*、/api/memory*:
| 职责 | 触发路径 | 行为 | Source |
|---|---|---|---|
| 未登录守卫 | /workspace/* | 无 cookie → 302 /api/acdm/auth/login | proxy.ts:81-89 |
| langgraph header 注入 | /api/langgraph/* | 解 cookie JWT → 注入 X-Auth-User-Id/Role | proxy.ts:57-79 |
| memory header 注入 | /api/memory* | 同上(UI memory 标签直连 gateway:8001) | proxy.ts:19-23 :77 |
injectAuthHeaders(proxy.ts:57-71)的容错:无 cookie / payload 无 sub 时 NextResponse.next() 原样放行,后端因缺头返 401(memory 路径则只是返回空 memory)——不在前端造假身份,验签责任永远在后端。
Common Pitfalls / 实战 Tips
/api/acdm/auth/*必须排除 forward_auth:Caddy 配置里这条 matcher 要在/api/acdm/*之前,否则登录请求也要鉴权 → 永远登不进去。- proxy.ts 不验签是有意的(
proxy.ts:16-17):它只搬运,验签在 acdm-backend。别在 proxy 里加 JWT 验签"加固",那是职责错位。 - dev 缺 cookie 时 chat 坏:
proxy.ts:14注明,dev 不注入头则acdm_auth.authenticate因缺 header raise 401,dev chat 完全不可用 —— 这正是 proxy 第 2 职责存在的原因。 - forward_auth 是同步阻塞子请求:每个受保护请求都多一次 verify 往返,verify 必须快(它只解 cookie + 一次 DB 查)。
References
acdm-backend/app/auth/verify.py:1-28— forward_auth verify 端点(产出 X-Auth-* 四头)deer-flow/frontend/src/proxy.ts:1-99— 前端三职责守卫/注入(本章主源)deploy/prod/Caddyfile.keycloak-v1— Caddy 路由 + forward_auth 配置openspec/specs/architecture/spec.md:104-115— 反代路由表与 forward_auth 矩阵acdm-backend/app/auth/router.py—/auth/loginKeycloak 接力deer-flow/backend/packages/harness/deerflow/auth/acdm_auth.py— 引擎侧从 X-Auth-* 提取身份
Related Pages
| Page | Relationship |
|---|---|
| 系统整体架构 | 本章是该章服务拓扑的单请求逐跳展开 |
| 鉴权与授权双层体系 | 本章 verify/Keycloak 在该章详解完整登录流与授权 |
| proxy 鉴权守卫与 API 反代 | 本章 proxy.ts 三职责在该章从前端视角再展开 |
| 控制面与引擎的耦合契约 | 本章 X-Auth-* 头如何被引擎消费在该章详解 |
| 本地开发环境搭建 | 本章 dev 无 Caddy 的 header 注入对应该章 dev 鉴权 |