FastAPI: 中间件 — 请求/响应的拦截与增强

最后更新:2026-08-26

中间件就像机场的安检通道——每个旅客(请求)必须依次通过海关、安检、登机口,任何一站可以放行也可以拦截,而回程(响应)按相反顺序经过。

1. 你将学到


2. Alice 的真实故事

(1) 痛点:API 被恶意刷量且无法追踪

Alice 的 PriceTracker API 上线后,发现某个 IP 每分钟发送 5000 次请求,导致数据库负载飙升。更糟糕的是,Bob 前端从 localhost:3000 调用 API 时被浏览器 CORS 策略拦截,所有请求失败。Alice 需要同时解决跨域访问和请求限流两个问题,但 Flask 没有统一的中间件机制。

(2) FastAPI 中间件的解法

FastAPI/Starlette 提供洋葱模型中间件,一层解决一个问题:CORS 中间件处理跨域,限流中间件控制频率,日志中间件记录请求——互不干扰,注册即生效。

PYTHON
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware

app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["http://localhost:3000"])

(3) 收益

Bob 前端跨域问题 5 行代码解决,限流中间件将恶意 IP 的请求从每分钟 5000 次降到 1000 次,日志中间件让每个请求都有唯一 ID 方便追踪。


3. 中间件洋葱模型

(1) 洋葱模型原理

100%
flowchart TD
    Request[Client Request] --> CORS[CORS Middleware]
    CORS --> Logging[Logging Middleware]
    Logging --> RateLimit[Rate Limit Middleware]
    RateLimit --> App[FastAPI Application]
    App --> RateLimit2[Rate Limit Response]
    RateLimit2 --> Logging2[Logging Response]
    Logging2 --> CORS2[CORS Response]
    CORS2 --> Response[Client Response]
    
    RateLimit -.->|429 Too Many Requests| Reject[Short-circuit Rejection]
    Reject --> Logging2
特性 说明
请求方向 从外到内(最先注册的最外层)
响应方向 从内到外(与请求相反)
短路能力 任何中间件可提前返回响应,不进入下一层
注册顺序 最后注册的中间件最先处理请求

▶ 示例:中间件注册顺序与执行顺序

PYTHON
from fastapi import FastAPI, Request
import time

app = FastAPI()

# Middleware registered FIRST = outermost layer (executes first on request)
@app.middleware("http")
async def outer_middleware(request: Request, call_next):
    print("Outer: before request")
    response = await call_next(request)
    print("Outer: after response")
    return response

# Middleware registered LAST = innermost layer (executes last on request)
@app.middleware("http")
async def inner_middleware(request: Request, call_next):
    print("Inner: before request")
    response = await call_next(request)
    print("Inner: after response")
    return response

@app.get("/test")
async def test():
    print("Handler: processing")
    return {"ok": True}

输出:

TEXT 📖 仅展示
Outer: before request
Inner: before request
Handler: processing
Inner: after response
Outer: after response

4. CORS 中间件

(1) 跨域资源共享配置

CORS(Cross-Origin Resource Sharing)是浏览器安全策略,限制不同源的网页访问 API。Bob 的前端(localhost:3000)访问 Alice 的 API(localhost:8000)属于跨域。

▶ 示例:开发环境 CORS 配置

PYTHON
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # Bob's frontend
    allow_credentials=True,
    allow_methods=["*"],  # All HTTP methods
    allow_headers=["*"],  # All headers
)

输出:

TEXT 📖 仅展示
# 执行成功

▶ 示例:生产环境 CORS 配置

PYTHON
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware

app = FastAPI()

ALLOWED_ORIGINS = [
    "https://pricetracker.example.com",
    "https://admin.pricetracker.example.com",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=ALLOWED_ORIGINS,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)

输出:

TEXT 📖 仅展示
# 执行成功
CORS 参数 开发环境 生产环境
allow_origins ["*"] 或 localhost 具体域名列表
allow_credentials True True(如需 Cookie)
allow_methods ["*"] 仅需要的方法
allow_headers ["*"] 仅需要的头
max_age 默认 3600(缓存预检)

5. 自定义中间件

(1) 请求计时中间件

▶ 示例:记录每个请求的处理时间

PYTHON
from fastapi import FastAPI, Request
import time

app = FastAPI()

@app.middleware("http")
async def timing_middleware(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"] = f"{process_time:.4f}s"
    return response

@app.get("/products")
async def list_products():
    return [{"id": 1, "name": "Widget"}]

输出:

TEXT 📖 仅展示
# 函数定义成功

▶ 示例:请求 ID 注入中间件

PYTHON
import uuid
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def request_id_middleware(request: Request, call_next):
    request_id = str(uuid.uuid4())
    request.state.request_id = request_id  # Attach to request state
    response = await call_next(request)
    response.headers["X-Request-ID"] = request_id
    return response

@app.get("/health")
async def health_check(request: Request):
    return {
        "status": "healthy",
        "request_id": request.state.request_id,
    }

输出:

TEXT 📖 仅展示
# 函数定义成功

▶ 示例:API 限流中间件(简易版)

PYTHON
from fastapi import FastAPI, Request, HTTPException
from collections import defaultdict
import time

app = FastAPI()

# Simple in-memory rate limiter
rate_limits: dict[str, list[float]] = defaultdict(list)
RATE_LIMIT = 1000  # requests per minute
WINDOW = 60  # seconds

@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    client_ip = request.client.host if request.client else "unknown"
    now = time.time()
    
    # Clean old entries
    rate_limits[client_ip] = [
        t for t in rate_limits[client_ip] if now - t < WINDOW
    ]
    
    # Check limit
    if len(rate_limits[client_ip]) >= RATE_LIMIT:
        raise HTTPException(
            status_code=429,
            detail=f"Rate limit exceeded: {RATE_LIMIT} requests per {WINDOW}s",
        )
    
    rate_limits[client_ip].append(now)
    response = await call_next(request)
    response.headers["X-RateLimit-Limit"] = str(RATE_LIMIT)
    response.headers["X-RateLimit-Remaining"] = str(
        RATE_LIMIT - len(rate_limits[client_ip])
    )
    return response

输出:

TEXT 📖 仅展示
# 函数定义成功

6. 类式中间件与 ASGI callable

(1) 类式中间件写法

对于更复杂的中间件逻辑,推荐用类式写法,直接操作 ASGI scope/receive/send。

▶ 示例:类式请求日志中间件

PYTHON
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
import logging
import time

logger = logging.getLogger("pricetracker")

class LoggingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next) -> Response:
        start = time.perf_counter()
        logger.info(f"Request: {request.method} {request.url.path}")
        
        response = await call_next(request)
        
        duration = time.perf_counter() - start
        logger.info(
            f"Response: {request.method} {request.url.path} "
            f"status={response.status_code} duration={duration:.4f}s"
        )
        return response

# Register class-based middleware
app = FastAPI()
app.add_middleware(LoggingMiddleware)

输出:

TEXT 📖 仅展示
# 函数定义成功

(2) 中间件类型对比

写法 适用场景 优点 缺点
@app.middleware("http") 简单拦截 代码少 仅处理 HTTP
BaseHTTPMiddleware 中等复杂度 可配置化 仅处理 HTTP
纯 ASGI 类 完全控制 WebSocket 也处理 代码复杂

❓ 常见问题

Q 中间件注册顺序为什么重要?
A 最后注册的中间件最先处理请求(最内层)。CORS 应最晚注册(最外层),确保所有响应都带 CORS 头。限流应较早注册(较内层),被拒绝的请求仍经过 CORS。
Q @app.middleware 和 add_middleware 能混用吗?
A 可以,但 @app.middleware 注册的中间件在 add_middleware 之前(更外层)。建议统一用一种写法。
Q 生产限流用什么方案?
A 简易内存限流仅适合单进程。生产环境用 Redis + slowapi 或自定义 Redis 限流中间件,支持分布式和多进程。
Q CORS 的 allow_origins 能用 * 吗?
A 开发环境可以,生产环境必须指定具体域名。allow_credentials=True 时不能用 *,浏览器会拒绝。
Q 中间件能修改请求体吗?
A 可以但需要先读取 body 再重新构造。BaseHTTPMiddleware 有已知问题(读取 body 后 stream 消耗),复杂场景建议用纯 ASGI 中间件。
Q 如何禁用某个中间件?
A 没有内置开关。建议用环境变量控制:if settings.ENABLE_RATE_LIMIT: app.add_middleware(RateLimitMiddleware)

📖 小节


📝 作业

  1. 基础题(难度⭐):为 PriceTracker 添加 CORS 中间件,允许 http://localhost:3000 跨域访问,在浏览器中验证 Bob 前端可以调用 API。提示:app.add_middleware(CORSMiddleware, ...)
  2. 进阶题(难度⭐⭐):实现请求计时中间件,在响应头中添加 X-Process-Time,并用 Swagger UI 观察响应头。提示:@app.middleware("http") + time.perf_counter()
  3. 挑战题(难度⭐⭐⭐):实现基于 IP 的限流中间件(每分钟 1000 次),超限时返回 429 状态码和 Retry-After 响应头,同时在响应头中显示剩余配额。提示:defaultdict(list) + 时间窗口清理

---|

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏