FastAPI 基于 Starlette,天然继承了 Starlette 强大的中间件(Middleware)机制。中间件是位于请求进入路由函数之前、响应发出之前的一道”关卡”,可以用来做请求日志记录、跨域处理、认证校验、限流、请求耗时统计等横切关注点。本文系统讲解 FastAPI 中间件的核心用法。

中间件的执行模型

FastAPI 中间件与 Starlette 完全一致:每个请求会按照添加顺序依次穿过各层中间件,到达路由处理函数后,再按相反顺序穿过中间件返回。下面这张示意图可以帮你理解:

1
请求 → [中间件A → 中间件B → 路由函数] → 中间件B → 中间件A → 响应

中间件本质上是一个 ASGI 应用,包裹着下一个 ASGI 应用(内层中间件或真正的路由应用)。理解了洋葱模型,中间件的开发就简单了。

内置中间件一览

FastAPI/Starlette 内置了几个常用的中间件,直接拿来用即可:

中间件 用途
CORSMiddleware 处理跨域资源共享
HTTPSRedirectMiddleware 强制 HTTP 跳转 HTTPS
TrustedHostMiddleware 只允许指定的 Host 头
GZipMiddleware 对响应体启用 GZip 压缩

CORS 中间件

前后端分离项目中,CORS 是最常用的中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
expose_headers=["X-Custom-Header"],
max_age=3600,
)

参数说明:

  • allow_origins:允许的源站列表,生产环境一定要指定具体域名,不要用 ["*"]
  • allow_credentials:是否允许携带 Cookie,设为 Trueallow_origins 不能用 *
  • allow_methods:允许的 HTTP 方法
  • allow_headers:允许的请求头
  • max_age:预检请求缓存时间(秒)

其他内置中间件

1
2
3
4
5
6
7
8
9
10
11
12
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.middleware.gzip import GZipMiddleware

# 强制 HTTPS(生产环境配合反向代理使用)
app.add_middleware(HTTPSRedirectMiddleware)

# 只允许指定 Host 访问
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["api.example.com"])

# GZip 压缩(需设置最小响应大小)
app.add_middleware(GZipMiddleware, minimum_size=1024)

自定义中间件

自定义中间件有两种写法:函数式(使用装饰器 @app.middleware)和类式(继承 BaseHTTPMiddleware)。

函数式中间件:请求日志

这是最简单的方式,适合轻量级需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
import time
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def log_middleware(request: Request, call_next):
"""记录每个请求的方法、路径和耗时"""
start = time.time()
response = await call_next(request)
elapsed = time.time() - start
print(f"{request.method} {request.url.path}{response.status_code} ({elapsed:.4f}s)")
return response

核心要点:

  • 装饰器必须指定 "http" 参数
  • call_next(request) 是调用下一层(内层中间件或路由函数),返回 Response 对象
  • call_next 之前写的代码在请求阶段执行,之后写的代码在响应阶段执行

类式中间件:请求限流器

当中间件逻辑较复杂、需要构造函数参数或状态保持时,推荐用类式写法。下面实现一个基于 IP 的简易令牌桶限流器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import time
from collections import defaultdict
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse

class RateLimitMiddleware(BaseHTTPMiddleware):
"""简易令牌桶限流:每个 IP 每秒最多 max_requests 次请求"""

def __init__(self, app, max_requests: int = 10, window: int = 1):
super().__init__(app)
self.max_requests = max_requests
self.window = window # 时间窗口(秒)
self.buckets: dict[str, list[float]] = defaultdict(list)

async def dispatch(self, request: Request, call_next):
client_ip = request.client.host
now = time.time()
bucket = self.buckets[client_ip]

# 清理窗口外的旧记录
self.buckets[client_ip] = [t for t in bucket if now - t < self.window]

if len(self.buckets[client_ip]) >= self.max_requests:
return JSONResponse(
status_code=429,
content={"detail": "请求过于频繁,请稍后再试"},
headers={"Retry-After": str(self.window)},
)

self.buckets[client_ip].append(now)
return await call_next(request)

注册使用:

1
app.add_middleware(RateLimitMiddleware, max_requests=20, window=1)

纯 ASGI 中间件:更高粒度控制

如果需要完全控制 ASGI 事件(如处理 WebSocket),可以直接写纯 ASGI 中间件:

1
2
3
4
5
6
7
8
9
10
11
from starlette.types import ASGIApp, Scope, Receive, Send

class CustomASGIMiddleware:
def __init__(self, app: ASGIApp):
self.app = app

async def __call__(self, scope: Scope, receive: Receive, send: Send):
if scope["type"] == "http":
# 仅在 HTTP 请求时干预
print(f"ASGI 中间件:{scope['method']} {scope['path']}")
await self.app(scope, receive, send)

中间件的顺序问题

中间件的执行顺序与 add_middleware 的调用顺序相同——先添加的在外层,先拦截请求、最后放行响应。

1
2
3
4
# 洋葱模型:A 在最外层
app.add_middleware(MiddlewareA) # 第 1 层(最外层)
app.add_middleware(MiddlewareB) # 第 2 层
app.add_middleware(MiddlewareC) # 第 3 层(最内层,最靠近路由)

请求链路:A → B → C → 路由,响应链路:路由 → C → B → A

最佳实践排序

  1. 最外层:日志、耗时统计(尽量覆盖所有请求)
  2. 次外层:CORS、安全头
  3. 次内层:认证鉴权
  4. 最内层:请求数据预处理、响应格式化

实际场景:响应头注入

一个常见的需求是为所有响应统一注入安全头:

1
2
3
4
5
6
7
8
@app.middleware("http")
async def security_headers_middleware(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Strict-Transport-Security"] = "max-age=31536000"
return response

中间件 vs 依赖注入

很多初学者会困惑:都是请求前执行的逻辑,中间件和 Depends 依赖注入有何区别?

维度 中间件 Depends 依赖注入
作用范围 全局,覆盖所有路由 可按路由、路由分组、全局配置
执行时机 在路由匹配之前 路由匹配之后
请求信息 仅有 Request 对象 有解析后的路径/查询/请求体参数
修改响应 可以直接修改 Response 不能直接修改 Response
适用场景 日志、CORS、安全头、限流 鉴权、数据库会话、参数校验

一句话总结:影响”所有请求”用中间件,影响”特定业务逻辑”用 Depends

小结

FastAPI 的中间件系统继承自 Starlette,简洁而强大。日常开发中:

  • 优先级最高的中间件是 CORS,前后端分离几乎必配
  • 函数式 @app.middleware("http") 覆盖 80% 的场景
  • 需要参数化或复杂逻辑时,使用类式 BaseHTTPMiddleware
  • 牢记洋葱模型,合理安排中间件添加顺序

掌握中间件,你就掌握了 FastAPI 请求生命周期的”总开关”。