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 提供了两种实现「横切关注点」的方式:中间件依赖注入。很多新手会疑惑:它们都可以在请求中执行额外逻辑,区别是什么?

简单来说:

  • 中间件作用于所有请求,适合全局的、与业务无关的通用处理(如日志、压缩、安全头)。
  • 依赖注入则按需绑定到特定路由,适合与路由参数或业务逻辑紧密相关的操作(如权限校验、数据库会话)。

具体对比如下:

特性中间件依赖注入
触发范围所有请求自动经过按需注入到路由参数
执行逻辑先注册的先处理请求、后处理响应与路由参数顺序一致
用途全局拦截、压缩、CORS、全局日志参数提取、单路由认证、数据库连接
响应控制可以提前返回/修改响应不能直接返回响应

核心优势

  1. 全局统一:避免在每个路由重复写相同逻辑,减少代码冗余。
  2. 关注点分离:将日志、安全等与业务解耦,让代码更清晰。
  3. 灵活复用:同一中间件可应用到多个项目,甚至打包成独立模块。
  4. 性能友好: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 中间件是实现全局横切关注点的利器。合理运用它可以大幅提升应用的安全性、性能和代码可维护性。最后再梳理一遍要点:

  1. 优先使用官方/内置中间件(CORS、GZip),它们久经考验,开箱即用。
  2. 轻量功能推荐 @app.middleware("http") 装饰器,复杂功能则继承 BaseHTTPMiddleware
  3. 严格控制注册顺序,并利用 skip_paths 等方式过滤不需要处理的路径。
  4. 生产环境充分测试,注意避免同步阻塞和隐藏内部错误详情。

掌握这些技巧之后,你就能像搭积木一样,通过组合不同中间件来构建出健壮、高效的 FastAPI 应用。


🔗 扩展阅读