DeerFlow Gateway 与鉴权:HTTP API 与用户隔离
网页里输入「查今天天气」并发送,浏览器实际打的是类似 POST /api/threads/{id}/runs/stream 的请求。服务器必须确认调用者身份、是否有权访问该 thread、是否伪造跨站请求,还要把流量路由到正确 handler。这些逻辑若复制到每个接口,既难维护也容易漏洞。DeerFlow 用 FastAPI 构建 Gateway,把路由分发、身份认证、权限校验、用户隔离和 CSRF 防护收成统一入口;进门之后才轮到 Runtime 起 run、SSE 推流。多用户并存时,「张三看不到李四的对话」也靠这一层兜住。
入口与几道门
backend/app/gateway/app.py 里 include_router 挂上 threads、runs、models、skills、memory、channels 等模块,URL 前缀与业务域一一对应。登录成功后(routers/auth.py)签发 JWT,写入 Cookie:httponly=True(前端 JS 读不到,降低 XSS 偷 token 风险),samesite="lax"(限制跨站携带 Cookie)。权限模型是 resource:action,例如 threads:read|write|delete、runs:create|cancel,路由上叠 @require_auth 与 @require_permission("threads", "read", owner_check=True):前者证明你是谁,后者决定你能进哪间房。
FastAPI 中间件后添加的先执行。代码里先 add_middleware(AuthMiddleware),再加 CSRFMiddleware,因此入站顺序是 CSRF → Auth → Router → 权限装饰器 → 业务。CSRF 采用 Double Submit:Cookie 中的 token 与请求头(如 X-CSRF-Token)必须用 secrets.compare_digest 一致,否则 403。攻击者或许能诱使浏览器发请求,但读不到你的 Cookie,对不上双 token。AuthMiddleware 注释写明它是 fail-closed 安全网:即使某个路由忘了 @require_auth,全局中间件仍会拦未认证请求。
AuthMiddleware 关键路径
公开路径(本地登录、/health、/docs 等)直接放行。IM Channel 等后台服务可用内部 auth header 进入,不走用户 Cookie,避免把机器人流量硬塞进浏览器会话模型。否则读 access_token Cookie,解析 JWT、验签、查过期、查用户是否仍存在;失败返回 401。成功后把用户放进 request.state.user 和 AuthContext,并调用 set_current_user 写入 ContextVar;finally 里 reset_current_user,避免污染后续请求。
1 | # deerflow/runtime/user_context.py(示意) |
ContextVar 是 asyncio 任务本地变量,天然按请求隔离。Repository 层 list_threads(user_id=AUTO) 一类接口会 resolve_user_id 后自动加 WHERE user_id = ?,忘记手动传参也不应串数据。权限装饰器还会对 thread_id 做 check_access;不属于当前用户时故意返回 404 而不是 403,避免告诉攻击者「这个 ID 存在只是你没权限」。装饰器与仓储两道墙叠在一起:第一道漏了,第二道仍按用户过滤。
举个具体例子:库里有 abc123 属于用户 A、def456 属于用户 B。用户 A 请求 GET /api/threads/abc123 时,AuthMiddleware 先把 A 写入 ContextVar;owner_check 查到该 thread 的主人是 A,放行;列表查询再带上 A 的 user_id 过滤。若 A 去撞 def456,装饰器直接 404,列表接口也根本枚举不到 B 的数据。隔离是默认行为,不靠「记得传 user_id」这种约定。
登录、吊销与首次启动
本地登录流程:按客户端 IP 限流(约 5 分钟内失败 5 次则锁定约 5 分钟)→ 校验密码哈希 → create_access_token(user_id, token_version=...) → _set_session_cookie。JWT 里带 token_version;用户改密码时版本号加一,旧 token 的 ver 对不上即视为吊销,其他设备会话一并失效。限流挡暴力猜密码,version 挡「改密后旧会话仍可用」。
首次启动 _ensure_admin_user:若管理员数为 0,不自动建默认账号,只日志提示访问 /setup 创建,避免默认密码被扫。从「无鉴权老版本」升级时,会把没有 user_id 的孤儿 thread 迁到 admin 名下,减少升级后数据悬空。
安全细节可以收成一张清单:HttpOnly Cookie 防脚本偷 token;SameSite 降低 CSRF 面;Double Submit 再卡一道写操作;token_version 支持改密吊销;资源越权用 404 藏存在性;登录失败按 IP 限流。单项都不稀奇,叠在 Gateway 入口才形成默认安全姿态。
Gateway 的职责边界清楚:谁可以进、进哪间房、写操作是否带 CSRF。它不负责 agent 怎么想,也不负责流式事件怎么存,那是 Runtime 的事。读代码建议顺序:app.py 中间件注册 → auth_middleware.py 的 dispatch → csrf_middleware.py → authz.py → user_context.py → 任一带 owner_check 的 router。把这条链跟通,用户隔离的实现就不再抽象;再往下接 IM Channels 时,也能看懂内部 token 为什么要绕开浏览器 Cookie。
DeerFlow Gateway 与鉴权:HTTP API 与用户隔离