Skip to content

第 8 章 · Cookie 与 Header 参数

本章目标:学会用 CookieHeader 声明参数读取请求中的 Cookie 与请求头,理解下划线自动转换规则,并掌握在响应中安全地设置 Cookie。

8.1 又一对"姐妹"参数类

CookieHeaderQueryPath 同属一个家族:它们都继承自同一个 Param 公共类,因此支持完全相同的默认值、校验和别名机制。

为什么必须显式声明

如果只写 session_id: str = None,FastAPI 会把参数解释为查询参数。要让它去读 Cookie 或请求头,必须用 Cookie() / Header() 显式指定来源——这四个类从 fastapi 导入时实际是返回特殊类的工厂函数。

python
from typing import Optional
from fastapi import FastAPI, Cookie, Header

app = FastAPI()


@app.get("/items/")
async def read_items(
    session_id: Optional[str] = Cookie(None),   # 读取名为 session_id 的 Cookie
    user_agent: Optional[str] = Header(None),   # 读取 User-Agent 请求头
    x_token: Optional[str] = Header(None),      # 读取 X-Token 请求头
):
    return {"session_id": session_id, "user_agent": user_agent, "x_token": x_token}

测试:

bash
curl http://127.0.0.1:8000/items/ \
     -H "User-Agent: my-app/1.0" -H "X-Token: abc123" \
     -b "session_id=xyz789"

8.2 下划线自动转换:convert_underscores

HTTP 标准头的命名习惯是连字符user-agentcontent-type),但 Python 变量名不允许出现 -Header 默认开启自动转换:把参数名中的 _ 映射到 -,同时 HTTP 头本身大小写不敏感,所以 user_agent 能匹配 User-Agent

python
from fastapi import FastAPI, Header

app = FastAPI()


@app.get("/header-strict/")
async def read_strict_header(
    # 某些自定义头确实带下划线(如 x_custom_key)时才需要关闭转换
    x_custom_key: str = Header(..., convert_underscores=False),
):
    return {"x_custom_key": x_custom_key}

关闭转换前想清楚

部分 HTTP 代理和服务器禁止使用带下划线的头(Nginx 默认会丢弃这类头),开启 convert_underscores=False 前先确认整条链路都放行。

重复出现的同名头(如多个 X-Token)可以用 Optional[List[str]] 接收为列表,这在声明单个 str 时只会取其中一个值。

读取靠参数声明,写入则通过 response.set_cookie()。给路径函数加一个 Response 类型参数即可(不需要手动 return 它):

python
from fastapi import FastAPI, Response

app = FastAPI()


@app.post("/login/")
async def login(response: Response, username: str):
    response.set_cookie(
        key="session_id",
        value="s-123",
        max_age=3600,       # 有效期(秒)
        httponly=True,      # 禁止 JS 读取,防 XSS 窃取
        secure=True,        # 仅 HTTPS 发送
        samesite="lax",     # 防 CSRF:跨站请求不携带
        path="/",
    )
    return {"msg": f"{username} 已登录"}

也可以直接 return JSONResponse(...) 时在其上调用 set_cookie。三个安全标志位是面试高频题:

标志作用
httponly浏览器禁止 JavaScript 通过 document.cookie 读取,缓解 XSS 盗取会话
secure仅在 HTTPS 连接上发送
samesitestrict/lax/none 三档,控制跨站请求是否携带,缓解 CSRF

删除 Cookie 用 response.delete_cookie(key)(内部就是设置一个过期时间为纪元的 Set-Cookie 头)。

8.4 典型场景:会话追踪与客户端信息

把本章知识组合成一个常见中间层需求——统计接口的客户端构成并维持简单会话:

python
import uuid
from fastapi import FastAPI, Cookie, Header, Response

app = FastAPI()


@app.get("/track/")
async def track(
    response: Response,
    visitor: str = Cookie(None),            # 老访客的标识
    accept_language: str = Header("unknown"),  # 客户端语言偏好
    referer: str = Header("direct"),           # 来源页
):
    if visitor is None:
        # 新访客:签发一个不可预测的随机 ID
        visitor = uuid.uuid4().hex
        response.set_cookie(
            key="visitor", value=visitor,
            max_age=30 * 24 * 3600,
            httponly=True, samesite="lax",
        )
    return {
        "visitor_id": visitor,
        "lang": accept_language.split(",")[0],
        "referer": referer,
    }

Swagger UI 的坑

浏览器对 Cookie 有特殊的安全管控:在 /docs 的交互文档里填了 Cookie 参数点 Execute 也发不出去(JS 无法随意触碰 Cookie)。测试 Cookie 参数请用 curl -b 参数或 Postman。

生产级会话管理不要手写 Cookie 逻辑,应使用第 14 章的 OAuth2/JWT 方案或 Starlette SessionMiddleware;本章的手动方式适合理解原理和小型内部工具。

8.5 本章小结

  • Cookie/HeaderQuery/Path 同源同能力,必须显式声明否则会被当作查询参数;
  • Header 默认把参数名的 _ 转成 -convert_underscores=False 可关闭,但要小心代理丢头的兼容性问题;
  • 写 Cookie 靠注入 response: Response 后调用 set_cookiehttponly + secure + samesite 是安全三件套;
  • 重复头用 List[str] 接收;
  • /docs 页面无法真正发送 Cookie,联调要用 curl/Postman。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 参数 user_agent: str = Header(None) 会去读取哪个请求头?

2. set_cookie 中设置 httponly=True 的目的是?

3. 为什么在 /docs 交互界面里填写 Cookie 参数后点 Execute 收不到值?

4. 客户端连续发送两个同名头 X-Token: a 和 X-Token: b,参数写成 x_token: str 时会发生什么?

🛠️ 动手实践

  1. 实现 /theme/ 接口:GET 读取 theme Cookie 返回当前主题色,POST 接收 theme 查询参数并写入 theme Cookie(有效期一年、httponly)。
  2. 写一个 /debug-headers/ 接口,用 Request.headers 直接遍历打印全部请求头,对比"声明式读取"与"字典式读取"两种风格。
  3. 给第 7 章的上传接口加上限流雏形:读取 X-Forwarded-For 头识别来源 IP(注意多级代理时它是逗号分隔列表,取第一个),同一 IP 一分钟内最多上传 10 次(内存计数即可)。

输入参数的最后一块拼图完成。下一章进入第 9 章 · 错误处理与自定义异常