文章目录
-
- 一、什么是中间件?用一句话讲清楚
- 二、中间件能做什么?
- 三、如何创建你的第一个中间件?
-
- [3.1 最简单的写法:使用装饰器](#3.1 最简单的写法:使用装饰器)
- [3.2 稍微高级的写法:类式中间件(继承 BaseHTTPMiddleware)](#3.2 稍微高级的写法:类式中间件(继承 BaseHTTPMiddleware))
- 四、多个中间件的执行顺序(非常重要!)
- [五、FastAPI 内置的常用中间件](#五、FastAPI 内置的常用中间件)
-
- [5.1 CORSMiddleware ------ 解决跨域问题](#5.1 CORSMiddleware —— 解决跨域问题)
- [5.2 GZipMiddleware ------ 自动压缩响应内容](#5.2 GZipMiddleware —— 自动压缩响应内容)
- [5.3 HTTPSRedirectMiddleware ------ 强制 HTTPS](#5.3 HTTPSRedirectMiddleware —— 强制 HTTPS)
- [5.4 TrustedHostMiddleware ------ 防止主机头攻击](#5.4 TrustedHostMiddleware —— 防止主机头攻击)
- 六、实战案例:打造一个生产级别的请求日志中间件
- 七、中间件的注意事项和常见坑
-
- [7.1 请求体只能读取一次](#7.1 请求体只能读取一次)
- [7.2 中间件注册顺序必须在路由注册之前](#7.2 中间件注册顺序必须在路由注册之前)
- [7.3 中间件与依赖注入的执行顺序](#7.3 中间件与依赖注入的执行顺序)
- [7.4 BaseHTTPMiddleware 可能影响 contextvar 传播](#7.4 BaseHTTPMiddleware 可能影响 contextvar 传播)
- [7.5 避免在中间件中做耗时操作](#7.5 避免在中间件中做耗时操作)
- [八、中间件 vs 依赖注入:什么时候用哪个?](#八、中间件 vs 依赖注入:什么时候用哪个?)
- 九、总结
一、什么是中间件?用一句话讲清楚
在学习 FastAPI 的过程中,你可能会反复听到"中间件"这个词。它听起来有点抽象,但理解起来其实非常简单。
中间件就是一段在"请求到达接口函数之前"和"响应返回给客户端之前"自动执行的代码。
打个比方:把 Web 应用想象成一家餐厅。客人(客户端)下单后,服务员(中间件)会先在订单上登记时间、检查客人是否有预约、记录特殊需求,然后把订单交给后厨(路由函数)。菜品做好后,服务员再次核对、打包、贴标签,最后送到客人手里。中间件就是这个"服务员"角色------它在请求进来和响应出去的两个关键节点上,自动帮你处理一些通用事务,而不需要每个菜品(接口)自己操心这些杂事。
FastAPI 基于 Starlette 构建,因此它天然继承了 Starlette 强大的中间件机制。这意味着你可以非常轻松地为整个应用添加"全局"的请求-响应处理逻辑。
二、中间件能做什么?
中间件擅长处理 "跨接口的通用逻辑" ,也就是那些几乎所有接口都需要、但不想在每个接口里重复写的代码。典型应用场景包括:
| 场景 | 说明 | 实际用途 |
|---|---|---|
| 📋 日志记录 | 记录每个请求的 IP、方法、路径和响应状态 | 审计、调试、用户行为分析 |
| ⏱️ 性能监控 | 测量每个请求的处理耗时 | 定位慢接口、优化性能 |
| 🔐 身份验证 | 检查请求中的 Token 是否有效 | 全局权限控制 |
| 🌐 跨域处理(CORS) | 添加跨域响应头 | 允许前端调用你的 API |
| 🎯 链路追踪 | 为每个请求生成唯一 ID,串联所有日志 | 分布式系统调试 |
| 🔒 安全加固 | 添加安全响应头(防 XSS、防点击劫持) | 提升应用安全性 |
| 📦 响应压缩 | 自动压缩响应体,减少传输数据量 | 节省带宽、加快响应 |
通过中间件,这些通用功能被集中管理,你的接口函数可以专注于业务逻辑,代码更加简洁、可维护。
三、如何创建你的第一个中间件?
3.1 最简单的写法:使用装饰器
FastAPI 提供了 @app.middleware("http") 装饰器,这是创建自定义中间件最直接的方式。
python
from fastapi import FastAPI, Request
import time
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
# ===== 请求到达接口之前执行的代码 =====
start_time = time.perf_counter()
# ===== 调用下一层(可能是下一个中间件,也可能是最终的路由函数)=====
response = await call_next(request)
# ===== 响应返回之前执行的代码 =====
process_time = time.perf_counter() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
@app.get("/")
async def root():
return {"message": "Hello World"}
代码拆解:
@app.middleware("http"):声明这是一个 HTTP 中间件,会在每个 HTTP 请求中生效。request: Request:当前请求对象,和你在接口函数中用的Request是同一个东西。call_next:一个回调函数,调用它会将请求传递给"下一层"(下一个中间件或路由函数),并返回一个响应对象。response = await call_next(request):这行代码是"分水岭"。之前的代码在请求阶段执行,之后的代码在响应阶段执行。
运行效果: 当你访问任何接口时,响应头中都会自动带上 X-Process-Time 字段,告诉你这次请求处理了多久。
3.2 稍微高级的写法:类式中间件(继承 BaseHTTPMiddleware)
当中间件逻辑比较复杂、需要配置参数时,可以继承 Starlette 的 BaseHTTPMiddleware 类来组织代码:
python
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
import logging
logger = logging.getLogger(__name__)
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# 请求阶段:记录请求信息
client_ip = request.client.host
method = request.method
url = request.url.path
logger.info(f"收到请求: {method} {url} 来自 {client_ip}")
# 继续处理请求
response = await call_next(request)
# 响应阶段:记录响应状态
logger.info(f"响应: {method} {url} 状态码 {response.status_code}")
return response
# 注册中间件
app.add_middleware(LoggingMiddleware)
这种写法更符合面向对象风格,便于维护和复用。
四、多个中间件的执行顺序(非常重要!)
当你注册了多个中间件时,它们的执行顺序遵循一个经典的洋葱模型。
规则:最后添加的中间件最先处理请求,最后处理响应;最先添加的中间件最后处理请求,最先处理响应。
用一个具体的例子来说明:
python
@app.middleware("http")
async def middleware_a(request: Request, call_next):
print("A - 请求阶段")
response = await call_next(request)
print("A - 响应阶段")
return response
@app.middleware("http")
async def middleware_b(request: Request, call_next):
print("B - 请求阶段")
response = await call_next(request)
print("B - 响应阶段")
return response
@app.middleware("http")
async def middleware_c(request: Request, call_next):
print("C - 请求阶段")
response = await call_next(request)
print("C - 响应阶段")
return response
实际打印顺序:
C - 请求阶段
B - 请求阶段
A - 请求阶段
(路由函数执行...)
A - 响应阶段
B - 响应阶段
C - 响应阶段
洋葱模型可视化:
请求进入 →
╭─────────────────────╮
╭┴─ Middleware C ──────╮
╭┴── Middleware B ───────╮
╭┴─── Middleware A ────────╮
│ │
│ 路由函数 (业务逻辑) │
│ │
╰───────────────────────────╯
← 响应返回
每一层中间件包裹着下一层,最外层的中间件(最后添加的)最先"拦截"到请求,最后"释放"响应。
提示: 这个顺序意味着你应该把需要最先执行的逻辑(如跨域处理)放在最后注册,把需要最后执行的逻辑(如日志记录)放在最先注册。实际开发中,建议将 CORS 中间件最先注册,确保跨域头能在其他中间件出错时也能正常返回。
五、FastAPI 内置的常用中间件
FastAPI 直接继承并暴露了 Starlette 的多个实用中间件,你不需要自己造轮子。
5.1 CORSMiddleware ------ 解决跨域问题
这是最常用的内置中间件,用于处理浏览器的跨域资源共享限制。
python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://your-frontend.com"], # 允许的前端域名
allow_credentials=True, # 允许携带 Cookie
allow_methods=["GET", "POST", "PUT", "DELETE"], # 允许的 HTTP 方法
allow_headers=["Authorization", "Content-Type"], # 允许的请求头
max_age=86400, # 预检结果缓存时间(秒)
)
为什么需要这个? 当你前后端分离部署时,前端(如 https://myapp.com)向你的 API(如 https://api.myapp.com)发请求,浏览器出于安全策略会拦截。CORS 中间件通过在响应中添加特定头部,告诉浏览器"这个请求是允许的"。
关键参数说明:
| 参数 | 说明 | 生产环境建议 |
|---|---|---|
allow_origins |
允许访问的域名列表 | 明确指定域名,不要用 ["*"] |
allow_credentials |
是否允许携带 Cookie | 设为 True 时不能与 ["*"] 同时使用 |
allow_methods |
允许的 HTTP 方法 | 按需指定,减少预检触发 |
allow_headers |
允许的自定义请求头 | 建议明确列出,如 ["Authorization", "Content-Type"] |
max_age |
预检结果缓存时间(秒) | 设置较大值(如 86400)可减少预检请求 |
特别注意: CORS 中间件必须最先注册(放在 add_middleware 的最前面),这样即使后续中间件出错,跨域头也能正常返回给浏览器。
5.2 GZipMiddleware ------ 自动压缩响应内容
启用 GZip 压缩后,JSON 等文本内容的响应体积会显著减小,节省带宽并加快传输速度。
python
from fastapi.middleware.gzip import GZipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=1000)
minimum_size 表示只有响应体超过指定字节数时才会压缩(默认 500 字节),避免对小数据做无意义的压缩。
5.3 HTTPSRedirectMiddleware ------ 强制 HTTPS
自动将所有 HTTP 请求重定向到 HTTPS,提升应用安全性。
python
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
app.add_middleware(HTTPSRedirectMiddleware)
5.4 TrustedHostMiddleware ------ 防止主机头攻击
只允许指定的主机名访问你的应用,防止恶意请求伪造主机头。
python
from starlette.middleware.trustedhost import TrustedHostMiddleware
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["yourdomain.com", "localhost"]
)
六、实战案例:打造一个生产级别的请求日志中间件
下面是一个接近生产级别的日志中间件,它包含了客户端 IP、请求耗时、慢请求告警等功能:
python
from fastapi import FastAPI, Request
import time
import logging
from loguru import logger
app = FastAPI()
# 配置日志格式
logger.add("app.log", rotation="500 MB", level="INFO")
SLOW_THRESHOLD = 1.0 # 慢请求阈值(秒)
@app.middleware("http")
async def production_logging_middleware(request: Request, call_next):
# 生成唯一请求 ID(方便链路追踪)
import uuid
request_id = str(uuid.uuid4())[:8]
# 获取客户端信息
client_ip = request.client.host if request.client else "unknown"
method = request.method
url = request.url.path
# 记录请求开始
logger.info(f"[{request_id}] → 收到请求: {method} {url} 来自 {client_ip}")
# 开始计时
start_time = time.perf_counter()
# 处理请求
try:
response = await call_next(request)
except Exception as e:
logger.error(f"[{request_id}] 请求处理异常: {e}")
raise
# 计算耗时
process_time = time.perf_counter() - start_time
# 记录响应信息
status_code = response.status_code
logger.info(
f"[{request_id}] ← 响应完成: {method} {url} "
f"状态码 {status_code} 耗时 {process_time:.3f}s"
)
# 慢请求告警
if process_time > SLOW_THRESHOLD:
logger.warning(
f"[{request_id}] ⚠️ 慢请求警告: {method} {url} "
f"耗时 {process_time:.3f}s 超过阈值 {SLOW_THRESHOLD}s"
)
# 添加响应头
response.headers["X-Request-ID"] = request_id
response.headers["X-Process-Time"] = str(process_time)
return response
这个中间件做了哪些事?
- 为每个请求生成唯一 ID,方便追踪
- 记录请求来源、方法和路径
- 测量处理耗时并记录
- 捕获异常并记录错误日志
- 对耗时过长的请求发出警告
- 在响应头中添加请求 ID 和处理时间
这样一套配置下来,你的 API 就具备了基本的可观测性。
七、中间件的注意事项和常见坑
7.1 请求体只能读取一次
这是初学者最容易踩的坑。Request 对象的 .body() 方法只能读取一次,因为请求体是一个流。如果你在中间件里读了请求体,后面的路由函数就再也读不到了。
解决方案: 要么只在中间件中读取并存储到 request.state 中,要么使用 ASGI 底层中间件来真正修改请求体(比较复杂)。
7.2 中间件注册顺序必须在路由注册之前
app.add_middleware() 必须在所有 @app.get()、@app.post() 等路由装饰器之前调用,否则中间件可能不会对所有路由生效。这是一个非常容易忽略的细节。
python
# ✅ 正确顺序
app = FastAPI()
app.add_middleware(SomeMiddleware) # 先注册中间件
@app.get("/") # 再注册路由
async def root():
...
# ❌ 错误顺序(中间件可能不生效)
app = FastAPI()
@app.get("/")
async def root():
...
app.add_middleware(SomeMiddleware)
7.3 中间件与依赖注入的执行顺序
FastAPI 中,请求的生命周期顺序为:
中间件(请求阶段)→ 依赖注入 → 路由函数 → 后台任务 → 依赖注入(yield 退出部分)→ 中间件(响应阶段)
也就是说,带 yield 的依赖项的退出代码会在中间件的响应阶段之前执行。如果你需要依赖注入和中间件协同工作,需要注意这个时序关系。
7.4 BaseHTTPMiddleware 可能影响 contextvar 传播
如果你使用了 contextvars(Python 的上下文变量,常用于传递请求级别的全局状态),需要注意 BaseHTTPMiddleware 可能会中断 contextvar 的传播。如果遇到相关 bug,可以考虑将依赖 contextvar 的中间件放在 BaseHTTPMiddleware 类型的中间件之前注册。
7.5 避免在中间件中做耗时操作
中间件会在每一个请求上执行,任何耗时操作都会直接影响整个 API 的响应速度。尽量保持中间件逻辑轻量,把复杂业务逻辑放到路由函数或后台任务中。
八、中间件 vs 依赖注入:什么时候用哪个?
很多初学者会困惑:中间件和依赖注入都能实现"请求前的预处理",它们有什么区别?
| 对比维度 | 中间件 | 依赖注入 |
|---|---|---|
| 作用范围 | 全局,所有请求都会经过 | 可以指定到特定接口或路由组 |
| 执行时机 | 在依赖注入之前执行 | 在中间件之后、路由函数之前执行 |
| 能否修改响应 | ✅ 可以在 call_next 之后修改 |
❌ 只能影响请求阶段的逻辑 |
| 典型用途 | 日志、跨域、安全头、性能监控 | 数据库会话、用户认证、参数校验 |
| 粒度 | 粗粒度,全局统一处理 | 细粒度,可精确控制哪些接口使用 |
选择建议:
- 需要全局生效且与业务逻辑无关 → 用中间件(如日志、CORS、压缩)
- 需要灵活选择哪些接口使用 → 用依赖注入(如数据库连接、用户验证)
九、总结
中间件是 FastAPI 中非常强大的一个特性,它让你可以在不修改业务代码的前提下,优雅地为整个应用添加全局功能。
核心要点回顾:
- 中间件 = 请求前 + 响应后自动执行的代码
- 使用
@app.middleware("http")快速创建自定义中间件 - 多个中间件遵循"洋葱模型"执行:最后添加的最先执行
- FastAPI 内置了 CORS、GZip、HTTPS 重定向等多个实用中间件
- 注意请求体只能读一次、注册顺序必须在路由之前等细节