Files
deerflow-code/offline-backend-20260512/backend/app/gateway/proxy_path.py
T
吉鑫升andClaude Opus 4.8 3178b4c45e fix(auth): CSRF/鉴权中间件兼容反向代理路径前缀 + 无会话请求豁免
清空浏览器缓存后(尤其 token 换登录)报「CSRF token missing」。根因:
反向代理把请求转发到带前缀的路径(如 /xxx/api/v1/auth/login/token),
FastAPI 用 root_path 剥前缀做路由,但 Starlette 中间件跑在路由之前,
看到的仍是带前缀的 path,导致 CSRF 豁免名单与公开路径白名单(精确/前缀
匹配)全部失配。清缓存前靠 localStorage 残留 token 当 Bearer 绕过 CSRF,
清掉后裸奔才暴露。

- 新增 proxy_path.app_relative_path:匹配前按 root_path / X-Forwarded-Prefix
  剥掉代理前缀(无前缀时 no-op)。
- csrf_middleware:should_check_csrf / is_auth_endpoint 改用应用相对路径;
  补上漏收的 /api/v1/auth/login/token 豁免;新增「无 access_token 会话
  cookie 则跳过 CSRF」兜底(无会话请求不存在可被跨站伪造利用的会话)。
- auth_middleware:公开路径判断改用应用相对路径,避免 /xxx/api/public/*
  分享页被误判 401。
- langgraph_auth:无 access_token cookie 时跳过 CSRF,与网关链路一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 15:51:01 +08:00

40 lines
1.9 KiB
Python

"""Resolve the application-relative request path behind a reverse proxy.
A reverse proxy may forward requests under a path prefix, e.g.
``/xxx/api/v1/auth/login/token``. FastAPI strips that prefix for **routing**
when it is told about it via the ASGI ``root_path`` (uvicorn ``--root-path`` /
``--proxy-headers``, or ``X-Forwarded-Prefix`` from the proxy) — which is why
the route still matches and the request reaches a handler.
Starlette middleware, however, runs *before* routing and sees the full,
prefixed ``request.url.path``. Middlewares that match that raw path against
fixed whitelists — the CSRF-exempt auth paths and the AuthMiddleware public
paths — then mis-match, so login and ``/api/public/*`` endpoints get wrongly
rejected (``CSRF token missing`` / ``401``) on a prefixed deployment.
``app_relative_path`` returns the path with the proxy prefix removed so that
whitelist matching is prefix-agnostic. It is a no-op when no prefix is present
(direct deployment), and tolerates the prefix already being stripped from the
path (it only strips when the path actually starts with the prefix).
"""
from fastapi import Request
def app_relative_path(request: Request) -> str:
"""Return ``request.url.path`` with any reverse-proxy prefix removed.
Prefix source order: ASGI ``root_path`` (set by the server when the proxy
forwards it), then the ``X-Forwarded-Prefix`` header (proxies that add a
prefix without informing the ASGI server). Returns ``"/"`` for an empty
result so downstream ``rstrip("/")`` / ``startswith`` checks stay sane.
"""
path = request.url.path or "/"
prefix = (request.scope.get("root_path") or request.headers.get("x-forwarded-prefix") or "").rstrip("/")
if prefix and path.startswith(prefix):
stripped = path[len(prefix):]
if not stripped:
return "/"
return stripped if stripped.startswith("/") else "/" + stripped
return path