FastAPI

FastAPI简介
FastAPI 由 Sebastian Ramirez 于 2018 年 12 月开发,是一个现代 Python 网络框架,在构建 API 方面非常高效。 它基于 Python 的类型提示功能,该功能自 Python 3.6 起添加。 它是 Python 最快的 Web 框架之一。
- 由于它基于 Starlette 和 Pydantic 库的功能,其性能名列前茅,与 NodeJS 和 Go 相当。
- 除了提供高性能之外,FastAPI 还提供显着的开发速度、减少代码中人为引起的错误、易于学习并且完全可用于生产。
- FastAPI 完全兼容著名的 API 标准,即 OpenAPI 和 JSON 模式。
FastAPI实例

创建项目
FastAPI → 存储位置及项目名称 → 创建虚拟环境 → Create

为什么要创建虚拟环境?
隔离项目运行环境,避免依赖冲突,保持全局环境的干净和稳定
FastAPI – 环境设置
要安装 FastAPI(最好在虚拟环境中),请使用 pip 安装程序。
pip3 install fastapiFastAPI 依赖于 Starlette 和 Pydantic 库,因此它们也会被安装。
安装 Uvicorn
FastAPI 不附带任何内置服务器应用程序。 要运行 FastAPI 应用程序,您需要一个名为 uvicorn 的 ASGI 异步服务器接口,所以也使用 pip 安装程序安装它。 它还将安装 uvicorn 的依赖项——asgiref、click、h11 和 typing-extension
pip3 install uvicorn创建 FastAPI 应用程序
- 声明 FastAPI 类的应用程序对象。
from fastapi import FastAPI
app = FastAPI()- 创建路由——路由就是URL 地址和处理函数之间的映射关系,它决定了当用户访问某个特定网址时,服务器应该执行哪段代码来返回结果。
@app.get("/")
async def root():
return {"message": "Hello World"}
运行项目
代码运行
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
# 启动FastAPI应用
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app", # 格式为「文件名:FastAPI实例名」,这样 reload 才能生效
host="127.0.0.1", # 只监听本机;需要局域网/手机访问可改为 0.0.0.0
port=8000,
reload=True, # 代码改动后自动重启,仅开发时使用
)命令行运行(建议)
uvicorn main:app --reload这是 FastAPI / Starlette 框架最常用的启动命令,作用是启动一个基于 Uvicorn 的 ASGI 服务器,运行你的 Python Web 应用。
uvicorn
轻量、高性能的 Python ASGI 异步服务器接口
main:app
模块名:应用实例 •
main→ 你的 Python 文件名main.py•app→ 你在代码里创建的 FastAPI 实例对象(通常写app = FastAPI())—reload • 作用:开发模式自动重载 • 效果:你修改代码后保存即生效,不用手动重启服务器 • 注意:只在开发时用,生产环境不要加
通过命令启动 uvicorn 服务器。
uvicorn main:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28720]
INFO: Started server process [28722]
INFO: Waiting for application startup.
INFO: Application startup complete.浏览器访问http://localhost/8000。 将在浏览器窗口中看到返回结果。

通过访问http://127.0.0.1:8000/docs得到FastAPI交互式文档

FastAPI 还支持 Redoc ( https://github.com/Redocly/redoc) 提供的另一种自动文档化方法。

FastAPI - 参数
参数就是客户端发送请求时附带的额外信息和指令
参数的作用是让同一个接口能根据不同的输入,返回不同的输出,实现动态交互

参数分类

FastAPI参数类型判断流程

路径参数
FastAPI 中,它被称为路径。 路径或路由是 URL 中第一个"/"之后的部分。
如果参数名出现在路径的 { } 里——>路径参数
使用带有变量参数的路径
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello/{name}")
async def root(name : str):
return {"message": f("Hello {name}")}启动 Uvicorn 服务器并访问 http://localhost:8000/hello/Tutorialspoint URL。 浏览器显示以下 JSON 响应。
{"name":"Tutorialspoint"}将变量路径参数更改为其他内容,例如 http://localhost:8000/hello/Python,
{"name":"Python"}多参数路由
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello/{name}/{age}")
async def hello(name,age):
return {"name": name, "age":age}如果浏览器地址栏中给出的URL是http://localhost:8000/hello/Ravi/20,那么Ravi和20的 数据将分别赋值给变量name和age。 浏览器显示以下 JSON 响应 −
{"name":"Ravi","age":"20"}路径参数校验Path(了解即可)
Path用来对路径参数补校验规则和文档描述
gt ——大于 lt ——小于 ge——大于等于 le ——小于等于
例子
# 定义请求的的路径,请求方式为get
@app.get("/book/{id}")
# 定义这个接口,接收一个参数id,数据类型为int
async def get_book(id: int=Path(...,gt=0,lt=101)):
# 返回响应结果
return {"id": id, "title": f"这是第{id}本书"}通过docs文档可以知道路径参数必须为int整形且0<id<101
查询参数
声明的参数名没有出现在路径的 { } 里,路径操作函数会把该参数自动解释为查询参数
- 位置:URL 中的 ? 之后由&拼接的查询字符串,格式为 key1=value1&key2=value2(如 ?page=1&sort=desc)。
- 作用:对资源集合进行过滤、排序、分页等操作(如查询“第1页、按时间降序”的书籍列表)。
- 常用方法:GET(用于获取资源集合的筛选结果)。
例
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
async def hello(name:str,age:int):
return {"name": name, "age":age}
docs检查会告诉您 FastAPI 已检测到 /hello 端点没有路径参数,但有查询参数

单击Try it out按钮,输入"xiaowang"和"18"作为值,然后按Execute执行按钮。 文档页面现在显示 Curl 命令、请求 URL 以及 HTTP 响应的正文和标头

查询参数校验Query(了解即可)
Query用来对查询参数补校验规则和文档描述
例
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/books/")
async def read_books(
# 需求:图书分类,默认值为 "Python开发",长度限制 5 ~ 255
category: str = Query(
"Python开发",
min_length=5,
max_length=255,
description="图书的分类名称"
),
# 需求:价格,限制大小范围 50 ~ 100
price: float = Query(
...,
ge=50,
le=100,
description="图书的价格范围"
)
):
return {
"category": category,
"price": price,
"message": "查询成功"
}通过docs文档可以知道查询参数category为str类型,长度限制 5 ~ 255。price为float类型大小范围 50 ~ 100
Pydantic 重点!
Pydantic 是一个用于 数据解析和验证的 Python 库。Pydantic 定义了 BaseModel 类。 它充当创建用户定义模型的基类

Pydantic 5 层用法,从最基础到 Agent 实战
- 第1层 定义模型
继承 BaseModel 就行。每个字段写 名字: 类型。
from pydantic import BaseModel
class ChatRequest(BaseModel):
message: str
temperature: float- 第2层 默认值与可选
规则和 Python 函数参数一样:有默认值就是可选,没有就是必填。这条规则同时决定了 /docs 里字段有没有标 "required"。
class ChatRequest(BaseModel):
message: str # 没默认值 → 必填
temperature: float = 0.7 # 有默认值 → 可选
system_prompt: str | None = None # 可选,且允许为 None- 第 3 层 · 用 Field 加约束和描述
from pydantic import BaseModel, Field
class ChatRequest(BaseModel):
message: str = Field(min_length=1, max_length=4000, description="用户消息")
temperature: float = Field(default=0.7, ge=0, le=2)min_length/max_length— 字符串长度ge/le— 大于等于 / 小于等于(gt/lt是不含等号)description— 会直接显示在/docs页面上
- 第 4 层 · 嵌套模型与集合(Agent 场景最常用)
class Message(BaseModel):
role: str # "user" / "assistant" / "system"
content: str
class ChatRequest(BaseModel):
messages: list[Message] # 模型列表
metadata: dict[str, str] = {} # 字典这就是对话历史的形态——一个 Message 列表。FastAPI 会自动把 JSON 数组里的每个元素都校验一遍,包括嵌套的 role 和 content。
- 实例与数据间转换
from pydantic import BaseModel
class Message(BaseModel):
role: str
content: str
m = Message(role="user", content="你好")
# ── 进来(类方法,注意是 Message. 不是 m.)──
Message.model_validate({"role": "user", "content": "你好"}) # → Message 实例
Message.model_validate_json('{"role":"user","content":"你好"}') # → Message 实例
# ── 出去(实例方法)──
m.model_dump() # {'role': 'user', 'content': '你好'} → dict
m.model_dump_json() # '{"role":"user","content":"你好"}' → strmodel_validate 系列 = 类工厂,负责"把外部不规整的数据校验成规整的 Message 实例";
odel_dump 系列 = 实例出口,负责"把 Message 实例还原成 dict/JSON" 。
一个必学参数:exclude_unset
class UpdateProfile(BaseModel):
nickname: str | None = None
bio: str | None = None
# 用户只想改昵称,只传了 nickname
req = UpdateProfile.model_validate({"nickname": "小王"})
req.model_dump() # {'nickname': '小王', 'bio': None} ← 坑在这
req.model_dump(exclude_unset=True) # {'nickname': '小王'} ← 正确exclude_unset=True 的含义是"只输出用户显式传过的字段",而不是"输出所有字段(没传的给默认值)"。
不用它的后果:你拿这个 dict 去更新数据库,会把用户没碰过的 bio 字段覆盖成 None。这是做 PATCH 类接口的经典事故。
三个坑
坑 1:model_dump_json() 返回的是字符串,不是 dict
return JSONResponse(content=req.model_dump_json()) # ❌ 变成一坨双重转义的字符串
return JSONResponse(content=req.model_dump()) # ✅ 要 dict 就用这个坑 2:model_dump() 是递归的
嵌套模型会被转成嵌套 dict,不会留下模型对象:
conv.model_dump()
# {'messages': [{'role': 'user', 'content': '你好'}]}
# ↑ 里面也是 dict,不是 Message 对象这通常是你想要的(正好能直接 json 序列化),但如果你期望"外层转 dict、内层还是模型",那就理解错了。
坑 3:model_validate 默认会做类型转换,不是严格校验
class Item(BaseModel):
price: float
Item.model_validate({"price": "9.9"}) # ✅ 自动把 "9.9" 转成 9.9
Item.model_validate({"price": "abc"}) # ❌ 转不了,抛 ValidationError
# 想要严格模式(字符串不接受):
Item.model_validate({"price": "9.9"}, strict=True) # ❌ 报错这个"宽松转换"通常是好事(前端传 "5" 也能接住),但如果你做的是金融金额、ID 这类不能容忍隐式转换的场景,记得开 strict=True。
Pydantic 对 AI Agent 工程师真正的杀手级价值——它不只是校验请求,而是你和 LLM 之间的契约层:

代码案例(一定要看懂)
from pydantic import BaseModel, Field
class SearchArgs(BaseModel):
"""网页搜索工具"""
# 必填参数:没有默认值,LLM 返回的 JSON 里如果缺 query 字段,校验直接失败
query: str = Field(description="搜索关键词")
# 可选参数:default=5 表示不传就用 5;ge=1 / le=20 是限制范围(大于等于 1、小于等于 20)
# LLM 若返回 limit=100,Pydantic 会立刻抛 ValidationError,防止非法值进到后面代码
limit: int = Field(default=5, ge=1, le=20, description="返回条数")
tools = [
{
"type": "function", # 固定写法:声明这是一个"函数调用"类型工具
"function": {
# 工具名字:LLM 决定调用哪个工具时,就是拿这个名字来匹配的
"name": "search",
# 工具说明:用类的文档字符串("""网页搜索工具""")当描述
# 注意:若类没写文档docstring,__doc__ 会是 None,OpenAI API 会拒绝这个工具
"description": SearchArgs.__doc__,
# 参数 schema:根据上面 SearchArgs 的字段定义自动生成 JSON Schema
# 它会包含 query 必填、limit 可选且范围 1~20、字段 description 等全部信息
"parameters": SearchArgs.model_json_schema(),
},
}
]
def parse_llm_arguments(llm_response: str) -> SearchArgs:
"""
把 LLM 丢回来的 JSON 字符串,经 Pydantic 校验后转成类型安全的 SearchArgs 实例。
"""
return SearchArgs.model_validate_json(llm_response)
if __name__ == "__main__":
# 真实项目中,这一串是 LLM 根据上面 tools 的 JSON Schema 自己生成的
llm_response: str = '{"query": "FastAPI 模板渲染", "limit": 3}'
# 回程:字符串 → 校验 → 对象
args = parse_llm_arguments(llm_response)
print("=== 回程结果(LLM JSON → 类型安全的 Python 对象)===")
print("query :", args.query) # 类型确定是 str,
print("limit :", args.limit) # 类型确定是 int
print("=== 去程产物(模型 → JSON Schema,这就是喂给 LLM 的 parameters)===")
import json
print(json.dumps(SearchArgs.model_json_schema(), ensure_ascii=False, indent=2))校验失败长什么样
不用你处理,FastAPI 自动返回 422:
{
"detail": [
{
"type": "missing",
"loc": ["body", "message"],
"msg": "Field required"
}
]
}loc 告诉你是哪个字段出的问题(嵌套模型会显示完整路径如 ["body", "messages", 0, "role"]),msg 说明原因。前端可以直接拿这个渲染错误提示。
请求体
- 位置:HTTP 请求的消息体(body)中,常以 JSON 等格式携带数据。
- 作用:用于创建、更新资源,或携带大量数据(如提交书籍的详细信息、更新用户资料)。
- 常用方法:POST(创建资源)、PUT(更新资源)等。
在HTTP协议中,一个完整的请求由三部分组成:
- 请求行:包含方法、URL、协议版本
- 请求头:元数据信息(Content-Type、Authorization等)
- 请求体:实际要发送的数据内容
我们现在将使用 Pydantic 模型对象作为客户端请求的请求体。
from fastapi import FastAPI
from typing import List
from pydantic import BaseModel, Field
app = FastAPI()
class Student(BaseModel):
id: int
name :str = Field(None, title="name of student", max_length=10)
subjects: List[str] = []
@app.post("/students/")
async def student_data(s1: Student):
return s1可以看出,student_data() 函数由 @app.post() 装饰器装饰,其 URL 端点为"/students/"。它从客户端的请求中接收一个 Student 类的对象作为 Body 参数。 要测试此路由,请启动 Uvicorn 服务器并通过访问 http://localhost:8000/docs 在浏览器中打开 Swagger UI 文档

文档指出"/students/"路由附加了带有 POST 方法的 student_data() 函数。 在 schemas 部分下,将列出 Student 模型。
展开它前面的节点以显示模型的结构

点击Try it out按钮,在请求主体中填写测试值。

单击Execute执行按钮并获取服务器的响应值。

练习
需求:设计接口新增图书,图书信息包含:书名、作者、出版社、售价
具体要求如下:
书名:不能为空;长度 2 ~ 20
作者:长度 2 ~ 10
出版社:默认值“小王出版社”
售价:不能为空;价格大于0元
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional
app = FastAPI()
# 1. 定义数据模型
class Book(BaseModel):
# 书名:不能为空;长度 2 ~ 20
book_name: str = Field(
...,
min_length=2,
max_length=20,
description="书名,必填,2-20字"
)
# 作者:长度 2 ~ 10
author: str = Field(
...,
min_length=2,
max_length=10,
description="作者,必填,2-10字"
)
# 出版社:默认值“黑马出版社”
publisher: str = Field(
default="小王出版社",
description="出版社,默认小王出版社"
)
# 售价:不能为空;价格大于0元
price: float = Field(
...,
gt=0,
description="价格,必填,大于0"
)
# 2. 定义新增图书接口
@app.post("/books", summary="新增图书")
async def create_book(book: Book):
"""
接收图书信息,校验通过后返回
"""
# 这里可以添加保存到数据库的逻辑
return {"message": "图书新增成功", "book_data": book}异常处理
FastAPI 异常处理的整体链路(从请求到响应)
一次请求大致会经历:
- 路由匹配(是否有对应 path + method)
- 依赖注入与参数解析
- 路径/查询/请求头/Cookie/Body 参数解析
- Pydantic 校验与类型转换
- 执行你的路径函数(endpoint)
- 返回值序列化为响应(response_model、JSON 序列化等)
- 中间件(Middleware)前后包裹(异常也会在这里被捕获/转换)
FastAPI 默认会处理的几类“常见异常”
A. 参数校验失败:422 Unprocessable Entity(最常见)
来源:Pydantic / 参数解析阶段
典型场景:
- Body 缺字段、类型不匹配
- Query/Path/Header 校验(例如
Query(ge=0)不满足) - 嵌套模型校验失败
表现:FastAPI 自动返回 422,响应结构通常是:
{
"detail": [
{
"loc": ["body", "fieldName"],
"msg": "Field required",
"type": "missing"
}
]
}要点:
loc指出错误位置(body/query/path/header + 字段路径)msg是人类可读信息- 这类异常本质是 RequestValidationError
B. 主动抛出 HTTPException:自定义状态码与错误信息
这是 FastAPI 推荐的“业务层错误抛法”。
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="资源不存在")特点:
- 会被框架直接转换成对应 status code 的 JSON 响应
detail是返回体核心字段(你也可以放 dict)
典型用法:
- 鉴权失败:401/403
- 资源不存在:404
- 业务规则不满足:400/409 等

C. 路由不存在:404 Not Found
当 path/method 不匹配时,Starlette 层返回 404。
D. 未捕获的运行时异常:500 Internal Server Error
比如你写的 endpoint 里 1/0、访问空对象等,没有 try/except,也没有全局 handler。
生产环境通常不返回 traceback 给客户端(避免泄露内部信息)。
FastAPI —UploadFile文件上传
- 读取方式——流式读取
- 获取的类型——文件对象
- 适合大小——任意大小
UploadFile:文件存在磁盘临时位置,不会把内存撑爆
from fastapi import FastAPI, UploadFile
app = FastAPI()
@app.post("/upload")
async def upload(file: UploadFile):
# 1. 读内容
content = await file.read()
# 2. 存到本地磁盘
with open(f"{file.filename}", "wb") as f:
f.write(content)
# 3. 返回信息给客户端
return {"filename": file.filename, "size": file.size or len(content), "type": file.content_type}用await file.read() 读取文件,再一次性写入磁盘。这对小文件没问题,但大文件会占满内存 。
shutil.copyfileobj 会 分块流式拷贝 (默认 64KB buffer),边读边写,内存占用恒定。
from fastapi import FastAPI, UploadFile
import shutil
app = FastAPI()
@app.post("/upload")
async def upload(file: UploadFile):
# 流式拷贝:边读边写,不占满内存
with open(file.filename, "wb") as f:
shutil.copyfileobj(file.file, f)
return {"filename": file.filename, "size": file.size, "type": file.content_type}多个文件
@app.post("/upload-many")
async def upload_many(files: list[UploadFile]):
return {"count": len(files)}⚠️ 两个安全点,别偷懒
第一,校验类型:
python
ALLOWED = {"image/png", "image/jpeg", "application/pdf"}
@app.post("/upload")
async def upload(file: UploadFile):
if file.content_type not in ALLOWED:
raise HTTPException(400, f"不支持的类型:{file.content_type}")第二,绝对不要直接用 file.filename 拼路径:
python
# ❌ 危险:用户传 "../../../etc/passwd" 就能写到系统目录
open(f"./uploads/{file.filename}", "wb")
# ✅ 自己生成文件名,只保留扩展名
import uuid, os
ext = os.path.splitext(file.filename)[1]
safe_name = f"{uuid.uuid4().hex}{ext}"
open(f"./uploads/{safe_name}", "wb")
"""
- `os.path.splitext(file.filename)[1]` —— 只拿扩展名。比如`../../../etc/passwd` 切出来 ext 是`""` (没有扩展名),`evil.png` 切出来是`.png` 。`../` 这些危险字符都在"主文件名"部分,被直接丢弃了。
- `uuid.uuid4().hex` —— 生成一个 32 位的随机十六进制字符串,比如`a3f5c2e1b9...` 。这串字符 只包含 0-9 和 a-f ,不可能出现`/` 、`..` 、空格等路径特殊字符。
- 拼起来 :`a3f5c2e1b9....png` —— 一个纯安全字符组成的文件名,无论用户传什么恶意路径,最终落到磁盘上的都是这种安全名字。
"""这叫路径穿越攻击,是文件上传功能的头号漏洞。
FastAPI - Header 标头参数
为了读取作为客户端请求的一部分的 HTTP header 标头的值,从 FastAPI 库中导入 Header 对象,并在操作函数定义中声明一个 Header 类型的参数。
from typing import Annotated
from fastapi import FastAPI, Header
app = FastAPI()
# 写法一:直接 `Header()` 作为默认值(经典写法)
# 注意:Header 不带默认值 = 必填,请求缺少 X-Token 头会直接返回 422
@app.get("/items")
async def read_items(x_token: str = Header()):
return {"token": x_token}
# 写法二:用 `Annotated` 声明(必填)
# `Annotated[str, Header()]` 把"类型"和"参数来源"打包在一起,类型信息更完整
# 不传 Authorization 头 → 422
@app.get("/required-header")
async def required_header(authorization: Annotated[str, Header()]):
return {"authorization": authorization}
# 写法三:`Annotated` + 可选
# 注意:`Annotated` 写法下默认值用 `= None` 设置,而不是 Header(default=None)
# 该头可以不传,此时参数值为 None,不会报错
@app.get("/optional-header")
async def optional_header(user_agent: Annotated[str | None, Header()] = None):
return {"user_agent": user_agent}
# 写法四:`Annotated` + 默认值
# 同理,默认值 "zh-CN" 写在 `= "zh-CN"`,不传 Accept-Language 头时自动使用
@app.get("/default-header")
async def default_header(accept_language: Annotated[str, Header()] = "zh-CN"):
return {"accept_language": accept_language}核心规则:下划线 ——> 连字符

HTTP 请求头用连字符(X-Token),Python 变量名不能含连字符(只能 x_token)。 FastAPI 自动帮你转换:
转换方向是单向的:参数名里的 _ 会被换成 -。所以:
- ✅
x_token: str = Header()→ 匹配X-Token - ❌ 如果你写
x-token: str = Header()→ Python 语法错误,根本写不出来
预定义标头。
操作函数应该有一个Response类型的参数。在以下情况下,将添加一个名为"X-Web-Framework"的自定义标头和一个预定义标头"Content-Language"以及操作功能的响应。
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/rspheader/")
def set_rsp_headers():
content = {"message": "Hello World"}
headers = {"X-Web-Framework": "FastAPI", "Content-Language": "en-US"}
return JSONResponse(content=content, headers=headers)校验与描述
Header() 接受和 Field() 完全一样的参数:
from typing import Annotated
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/key")
async def read_key(x_api_key: Annotated[str, Header(
min_length=10,
max_length=64,
description="你的 API Key,在控制台获取",
deprecated=False,
)]):
return {"api_key": x_api_key}description 会直接显示在 /docs 页面,这对使用方非常友好
实战:Bearer Token 鉴权
这是 Header() 在 Agent 项目里 90% 的用途:
from fastapi import Header, HTTPException
API_KEYS = {"sk-abc123": "user-001", "sk-def456": "user-002"}
@app.post("/chat")
async def chat(
req: ChatRequest,
authorization: str | None = Header(
default=None,
description="Bearer <你的 API Key>",
),
):
if authorization is None:
raise HTTPException(401, "缺少 Authorization 请求头")
if not authorization.startswith("Bearer "):
raise HTTPException(401, "格式错误,应为:Bearer <token>")
token = authorization.removeprefix("Bearer ").strip()
user_id = API_KEYS.get(token)
if user_id is None:
raise HTTPException(
status_code=401,
detail="无效的 API Key",
headers={"WWW-Authenticate": "Bearer"}, # 401 的标准响应头
)
# 鉴权通过,user_id 可以用于限流、计费、日志
return await run_agent(req, user_id=user_id)几个细节值得注意:
removeprefix("Bearer ")比replace("Bearer ", "")更安全——后者会把 token 中间的 "Bearer " 也替换掉.strip()去掉可能的多余空格- 401 响应带
WWW-Authenticate: Bearer是 HTTP 标准要求,客户端库会据此识别认证方式
常见真实请求头清单
| 请求头 | 用途 | Agent 相关性 |
|---|---|---|
Authorization | 鉴权 token(Bearer / Basic) | 高 |
X-API-Key | 简单 API Key | 高 |
X-Request-ID | 请求追踪,配合中间件打日志 | 高 |
Idempotency-Key | 幂等键,防重复调用 | 中 |
User-Agent | 客户端标识、限流依据 | 中 |
Accept-Language | 语言偏好 | 低 |
X-Forwarded-For | 真实客户端 IP(在 Nginx 后面时) | 中 |
四个坑
坑 1:命名写错不报错,只是取不到值
坑 2:自定义请求头会触发 CORS 预检
这个在 Agent 项目里特别容易中招。跨域调用时,浏览器发现请求带了非简单头(Authorization、X-Request-ID 都属于此类),会先发一个 OPTIONS 预检请求。如果你的 CORS 配置没放行,浏览器会直接拦掉——后端连日志都不会有,你会以为是接口写错了。
python
app.add_middleware(
CORSMiddleware,
allow_origins=["https://your-frontend.com"],
allow_headers=["Authorization", "X-Request-ID"], # ← 自定义头必须显式列出
# 开发阶段图省事可以写 ["*"]
)坑 3:请求头只能装字符串,塞不了 JSON
python
# ❌ 前端没法把对象放进请求头
X-Config: {"a": 1}
# ✅ 只能塞扁平字符串,需要结构化数据就编码
X-Config: eyJhIjoxfQ== # base64 编码后的 JSON真需要传结构化数据,请走请求体,不要硬塞请求头。
坑 4:别把敏感内容写进日志
Authorization 头里装的就是 token。如果你的中间件无脑打印所有请求头,等于把用户密钥写进了日志文件。打日志时记得脱敏:
python
safe_headers = {
k: ("***" if k.lower() == "authorization" else v)
for k, v in request.headers.items()
}一个进阶替代方案
实际项目里,鉴权更推荐用 Security + HTTPBearer,因为它会自动在 /docs 页面加上一个"Authorize"按钮,测试接口时不用手动敲请求头:
python
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import Security
bearer = HTTPBearer()
@app.post("/chat")
async def chat(
req: ChatRequest,
creds: HTTPAuthorizationCredentials = Security(bearer),
):
token = creds.credentials # 已经帮你剥掉 "Bearer " 了差别:Header() 给你原始字符串(完全可控),Security(HTTPBearer()) 给你解析好的凭据 + 文档集成(更省事)。建议先掌握 Header() 的原理,实际项目用 HTTPBearer。
总结一句话:Header() 的核心就三件事——下划线转连字符、请求头名不区分大小写、值永远是字符串。把这三条记住,剩下的都是 Field() 那套校验参数的复用。
