FastAPI中间件完全指南
📂 所属阶段:第二阶段 — 进阶黑科技(核心篇)
🔗 相关章节:FastAPI异步编程深度解析 · FastAPIexception-handling
目录
一、中间件基础入门
什么是中间件?
你可以把中间件想象成包裹在整个 FastAPI 应用外面的一层“洋葱皮”。每一个请求在到达真正的路由处理函数之前,都必须穿过这层外皮;而当处理函数生成响应后,响应又会按相反的顺序再次穿过这些外皮,最后才返回给客户端。
从技术角度看,中间件就是一个在请求和响应之间执行的钩子函数,它可以:
- 在请求到达路由之前做一些预处理(例如身份校验、日志记录)
- 在路由生成响应之后对其进行二次加工(例如压缩、添加安全头)
这样一来,所有路由都自动获得了这些能力,你再也不用在每个接口里重复编写同样的逻辑了。
请求生命周期中的位置
下图展示了三个不同的中间件(日志、CORS、GZip)在请求/响应管道中的执行顺序。请求自上而下传递,响应则自下而上返回。
sequenceDiagram
participant Client as 客户端
participant M1 as 中间件1(日志)
participant M2 as 中间件2(CORS)
participant M3 as 中间件3(GZip)
participant Route as 路由处理函数
Client->>M1: 请求
M1->>M2: 请求
M2->>M3: 请求
M3->>Route: 请求
Route-->>M3: 响应
M3-->>M2: 压缩后的响应
M2-->>M1: CORS响应
M1-->>Client: 带日志的响应
中间件 vs 依赖注入
FastAPI 提供了两种实现「横切关注点」的方式:中间件和依赖注入。很多新手会疑惑:它们都可以在请求中执行额外逻辑,区别是什么?
简单来说:
- 中间件作用于所有请求,适合全局的、与业务无关的通用处理(如日志、压缩、安全头)。
- 依赖注入则按需绑定到特定路由,适合与路由参数或业务逻辑紧密相关的操作(如权限校验、数据库会话)。
具体对比如下:
核心优势
- 全局统一:避免在每个路由重复写相同逻辑,减少代码冗余。
- 关注点分离:将日志、安全等与业务解耦,让代码更清晰。
- 灵活复用:同一中间件可应用到多个项目,甚至打包成独立模块。
- 性能友好:FastAPI 的中间件天然支持异步
async/await,不会阻塞事件循环。
二、中间件开发基础
FastAPI(底层基于 Starlette)提供了两种编写中间件的方式,分别适用于不同复杂度的场景。
方式1:装饰器中间件(轻量快捷)
如果你只需要实现一个非常简单的功能,比如统计每个接口的耗时,并且不需要初始化配置或内部状态,那么直接用 @app.middleware("http") 装饰器是最快的方式。
from fastapi import FastAPI, Request
import time
app = FastAPI()
@app.middleware("http")
async def add_process_time(request: Request, call_next):
# ── 请求到达时的预处理 ──
start = time.perf_counter()
# ── 将请求交给下一层(可能是下一个中间件,也可能是路由) ──
response = await call_next(request)
# ── 响应返回后的后处理 ──
cost = (time.perf_counter() - start) * 1000
response.headers["X-Process-Time"] = f"{cost:.2f}ms"
return response
💡 提示:call_next 负责调用下一个处理环节,最终会执行到路由函数。你可以在它前后分别插入逻辑,这就是中间件的核心模式。
方式2:继承 BaseHTTPMiddleware(适合复杂功能)
当你的中间件需要接收初始化参数(例如指定跳过哪些路径)或者维护内部状态(比如统计一些指标),就应该继承 Starlette 的 BaseHTTPMiddleware 类。
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class SimpleLogMiddleware(BaseHTTPMiddleware):
"""带初始化参数的简单日志中间件"""
def __init__(self, app, skip_paths: list = None):
super().__init__(app)
# 允许指定不需要打印日志的路径
self.skip_paths = skip_paths or ["/health", "/docs", "/redoc"]
async def dispatch(self, request: Request, call_next) -> Response:
# 跳过指定路径
if request.url.path in self.skip_paths:
return await call_next(request)
# 记录请求信息
logger.info(f"📥 {request.method} {request.url.path}")
response = await call_next(request)
# 记录响应状态
logger.info(f"📤 {request.method} {request.url.path} → {response.status_code}")
return response
# 注册中间件,同时传入自定义参数
app.add_middleware(SimpleLogMiddleware, skip_paths=["/metrics", "/docs"])
📌 注意:dispatch 方法的作用与装饰器版本完全一样,只是写法不同。你可以在 dispatch 内安全地访问 self 来读取初始化配置。
三、常用官方/内置中间件
FastAPI/Starlette 生态中已经内置了一些非常实用的中间件,开发时请优先考虑使用它们,避免重复造轮子。
1. CORS 跨域中间件
CORSMiddleware 是 FastAPI 官方维护的中间件,专门解决浏览器的跨域限制。配置得当可以避免一系列奇怪的前后端联调问题。
生产环境推荐配置
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
# ✅ 明确指定允许的域名,不要用通配符 *
allow_origins=[
"https://daomanpy.com",
"https://www.daomanpy.com",
"http://localhost:3000", # 开发环境临时加
],
# ✅ 允许携带 Cookie(必须配合明确的 allow_origins)
allow_credentials=True,
# ✅ 限制允许的 HTTP 方法
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
# ✅ 限制允许的请求头
allow_headers=["Content-Type", "Authorization", "X-Request-ID"],
# ✅ 暴露给前端的响应头
expose_headers=["X-Process-Time", "X-Request-ID"],
# ✅ 预检请求缓存 1 小时,减少重复请求
max_age=3600,
)
⚠️ 安全提醒:在生产环境切忌使用 allow_origins=["*"],尤其是当你同时设置了 allow_credentials=True 时——这会导致浏览器直接拒绝跨域请求。
2. GZip 响应压缩中间件
GZipMiddleware 来自 Starlette,它可以自动对 JSON、HTML、JS 等文本类响应进行压缩,显著减少网络传输量。压缩率通常能达到 70% 以上。
from starlette.middleware.gzip import GZipMiddleware
# 仅压缩大小超过 1000 字节的响应,避免对小数据做无谓的 CPU 开销
app.add_middleware(GZipMiddleware, minimum_size=1000)
配置极其简单,通常只需一行代码即可开启全局压缩。
四、高级自定义中间件实现
掌握了基础用法后,我们来看几个真实业务中非常实用的自定义中间件。
1. 安全头中间件
OWASP 等安全组织推荐在响应中加入一系列安全头部,用以防御常见的 web 攻击。我们可以通过一个中间件统一添加这些头部。
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
response = await call_next(request)
# 添加安全头部
response.headers.update({
"X-Content-Type-Options": "nosniff", # 禁止浏览器自动猜测 MIME 类型
"X-Frame-Options": "DENY", # 禁止被 iframe 嵌入
"X-XSS-Protection": "1; mode=block", # 开启 XSS 防护
"Referrer-Policy": "strict-origin-when-cross-origin", # 限制 Referrer 信息
})
return response
app.add_middleware(SecurityHeadersMiddleware)
这样一来,无论哪个接口返回的响应,都会自动带上这些保护头部。
2. 统一错误处理中间件
在大型项目中,我们通常会捕获所有未被路由层处理的异常,将其转化为结构化的 JSON 错误对象,避免向客户端暴露内部堆栈细节。
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import JSONResponse
import traceback
import os
DEBUG = os.getenv("ENVIRONMENT") != "production"
class UnifiedErrorHandler(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
try:
return await call_next(request)
# FastAPI 参数校验失败
except RequestValidationError as e:
return JSONResponse(
status_code=422,
content={"code": 422, "msg": "参数验证失败", "detail": e.errors()}
)
# HTTP 协议层面异常(如 404)
except StarletteHTTPException as e:
return JSONResponse(
status_code=e.status_code,
content={"code": e.status_code, "msg": e.detail}
)
# 其他未捕获的服务器内部错误
except Exception as e:
if DEBUG:
return JSONResponse(
status_code=500,
content={"code": 500, "msg": "内部错误", "traceback": traceback.format_exc()}
)
else:
return JSONResponse(
status_code=500,
content={"code": 500, "msg": "服务器开小差了,请稍后再试"}
)
app.add_middleware(UnifiedErrorHandler)
🔒 生产环境建议:DEBUG 模式由环境变量控制,线上环境务必隐藏详细的错误堆栈,防止泄露代码敏感信息。
五、生产环境最佳实践
中间件虽好,用不好也会带来麻烦。下面总结几条核心实践准则。
1. 控制中间件的注册顺序
请求会按照 app.add_middleware() 的调用顺序依次经过各中间件;响应则会反向经过。因此顺序至关重要:
- CORS 中间件通常放在靠前的位置,以便尽早拦截并处理浏览器的预检请求(
OPTIONS)。
- 错误处理中间件必须放在最后,这样才能捕获它前面所有中间件以及路由内部抛出的异常。
# ✅ 正确的注册顺序
app.add_middleware(SecurityHeadersMiddleware) # 安全头最先注入
app.add_middleware(CORSMiddleware, **cors_config)
app.add_middleware(GZipMiddleware, minimum_size=1000)
app.add_middleware(PerformanceMonitoringMiddleware) # 假设已定义
app.add_middleware(UnifiedErrorHandler) # exception-handling守在最后
2. 避免中间件中的性能陷阱
- 不要执行耗时操作:比如在中间件里查数据库、调用外部 API,除非这是必要的全局安全检查。如果一定要做,请确保是异步操作。
- 快速跳过无关路径:对
/docs、/health 等路径尽早返回 call_next,避免浪费计算资源。
- 保持异步:所有 IO 操作都应当使用
async/await,否则会阻塞整个事件循环,拖垮整个应用。
六、总结
FastAPI 中间件是实现全局横切关注点的利器。合理运用它可以大幅提升应用的安全性、性能和代码可维护性。最后再梳理一遍要点:
- 优先使用官方/内置中间件(CORS、GZip),它们久经考验,开箱即用。
- 轻量功能推荐
@app.middleware("http") 装饰器,复杂功能则继承 BaseHTTPMiddleware。
- 严格控制注册顺序,并利用
skip_paths 等方式过滤不需要处理的路径。
- 生产环境充分测试,注意避免同步阻塞和隐藏内部错误详情。
掌握这些技巧之后,你就能像搭积木一样,通过组合不同中间件来构建出健壮、高效的 FastAPI 应用。
🔗 扩展阅读