FastAPI重要知识点---中间件(Middleware)

文章目录

    • 一、什么是中间件?用一句话讲清楚
    • 二、中间件能做什么?
    • 三、如何创建你的第一个中间件?
      • [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

这个中间件做了哪些事?

  1. 为每个请求生成唯一 ID,方便追踪
  2. 记录请求来源、方法和路径
  3. 测量处理耗时并记录
  4. 捕获异常并记录错误日志
  5. 对耗时过长的请求发出警告
  6. 在响应头中添加请求 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 中非常强大的一个特性,它让你可以在不修改业务代码的前提下,优雅地为整个应用添加全局功能。

核心要点回顾:

  1. 中间件 = 请求前 + 响应后自动执行的代码
  2. 使用 @app.middleware("http") 快速创建自定义中间件
  3. 多个中间件遵循"洋葱模型"执行:最后添加的最先执行
  4. FastAPI 内置了 CORS、GZip、HTTPS 重定向等多个实用中间件
  5. 注意请求体只能读一次、注册顺序必须在路由之前等细节
相关推荐
小夏子_riotous2 小时前
Docker学习路径——3、常用命令
linux·运维·服务器·学习·docker·容器·centos
STLearner2 小时前
WSDM 2026 | 时间序列(Time Series)论文总结【预测,表示学习,因果】
大数据·论文阅读·人工智能·深度学习·学习·机器学习·数据挖掘
redaijufeng2 小时前
网络爬虫学习:应用selenium获取Edge浏览器版本号,自动下载对应版本msedgedriver,确保Edge浏览器顺利打开。
爬虫·学习·selenium
腾科IT教育2 小时前
零基础快速上岸HCIP,高效学习思路分享
学习·华为认证·hcip·hcip考试·hcip认证
23471021273 小时前
4.14 学习笔记
笔记·python·学习
醇氧3 小时前
【学习】软件过程模型全解析:从瀑布到敏捷的演进之路
学习·log4j
邪修king3 小时前
UE5 零基础入门第三弹: 碰撞与触发交互,解锁场景机关与蓝图封装(高娱乐性学习)
学习·ue5·交互
程序员老邢4 小时前
【人生底稿・番外篇 05】我的电影江湖:从录像带时代,到港片陪伴的青春岁月
java·程序人生·职场发展·娱乐
skylijf4 小时前
2026 高项第 6 章 预测考点 + 练习题(共 12 题,做完稳拿分)
笔记·程序人生·其他·职场和发展·软件工程·团队开发·产品经理