FastAPI 中间件详解:拦截与处理请求/响应的利器

在构建现代 Web 应用时,我们常常需要在请求处理流程的多个环节执行一些通用的、与核心业务逻辑无关的操作,比如记录日志、验证用户身份、压缩响应、处理跨域等。中间件(Middleware) 正是解决这类“横切关注点”(Cross-Cutting Concerns)的利器。FastAPI 提供了一套简洁而强大的中间件系统,让你能够轻松地在请求到达路由函数之前和响应返回给客户端之前插入自定义逻辑。

本文将带你深入理解 FastAPI 中间件的工作原理、核心用法、实战技巧以及最佳实践。


一、什么是中间件?—— API 的“守门人”与“质检员”

你可以将中间件想象成一条处理流水线上的多个检查站。每个 HTTP 请求在到达最终的“目的地”(即你的路由处理函数)之前,必须依次通过这些检查站;同样,每个响应在返回给客户端之前,也会反向经过这些检查站。

1. 中间件的核心职责

一个典型的 FastAPI 中间件函数会执行以下步骤:

  1. 接收请求:捕获进入的 Request 对象。
  2. 预处理:执行任意操作,如:
    • 记录请求日志。
    • 验证用户身份(认证)。
    • 检查请求头(如 Content-Type)。
    • 修改请求对象(如添加额外属性)。
  1. 调用下一个:调用 call_next(request),将请求传递给链条中的下一个中间件或最终的路由处理函数。
  2. 接收响应:等待并接收从下游返回的 Response 对象。
  3. 后处理:对响应进行操作,如:
    • 添加自定义响应头(如处理时间)。
    • 压缩响应体。
    • 格式化响应内容。
    • 记录响应状态和耗时。
  1. 返回响应:将(可能被修改的)响应返回给客户端。

2. 形象类比

  • 机场安检:每位乘客(请求)都必须经过安检(中间件)才能登机(到达路由函数)。安检可以检查证件(认证)、扫描行李(数据检查)、记录信息(日志)。登机后,可能还有贵宾室服务(响应处理)。
  • 快递分拣中心:包裹(请求)进入中心后,会被扫描(日志)、检查是否符合运输标准(验证)、贴上标签(修改请求),然后发往下一站。到达目的地前,可能还会进行最终打包(压缩响应)和贴上签收单(添加响应头)。

二、FastAPI 中间件基础:创建你的第一个“过滤器”

1. 创建一个简单中间件

这是定义中间件的标准方式。使用 @app.middleware("http") 装饰器。

import time
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse

app = FastAPI()

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    """
    这是一个中间件示例,用于测量每个请求的处理时间,并将其添加到响应头中。
    
    Args:
        request (Request): 当前的 HTTP 请求对象。
        call_next (Callable): 一个可调用对象,用于调用链条中的下一个中间件或路由函数。
    
    Returns:
        Response: 处理后的响应对象。
    """
    # --- 请求处理前 (Pre-processing) ---
    start_time = time.time()
    print(f"Middleware: 请求开始 - {request.method} {request.url}")

    # --- 调用下一个中间件或路由函数 ---
    # 这是关键!必须调用 call_next 才能让请求继续传递。
    # 注意:必须使用 await,因为 call_next 是一个异步生成器。
    try:
        response: Response = await call_next(request)
    except Exception as e:
        # 最佳实践:在中间件中捕获异常,可以进行统一的日志记录或返回自定义错误响应
        print(f"Middleware: 请求处理过程中发生异常: {e}")
        # 你也可以在这里返回一个错误响应,阻止异常向上传播
        # return JSONResponse(status_code=500, content={"detail": "Internal Server Error"})
        raise  # 重新抛出异常,让上层的异常处理器处理

    # --- 响应处理后 (Post-processing) ---
    # 计算处理时间
    process_time = time.time() - start_time
    # 将处理时间(秒)添加到响应头
    response.headers["X-Process-Time"] = str(process_time)
    # 也可以添加毫秒
    response.headers["X-Process-Time-ms"] = f"{process_time * 1000:.2f}ms"

    print(f"Middleware: 请求结束 - 处理耗时: {process_time:.4f}s, 状态码: {response.status_code}")

    # --- 返回响应 ---
    return response

@app.get("/")
async def main():
    # 模拟一些处理延迟
    await asyncio.sleep(0.1)
    return {"message": "Hello World"}

2. 关键组件解析

  • @app.middleware("http"):
    • 这是注册 HTTP 中间件的装饰器。目前 FastAPI 主要支持 "http" 类型的中间件。
  • request: Request:
    • FastAPI 提供的 Request 对象,封装了所有关于当前 HTTP 请求的信息,如方法、URL、头、查询参数、路径参数、客户端信息等。
  • call_next:
    • 这是一个可等待的(awaitable) 函数,代表中间件调用链中的下一个环节。必须调用 await call_next(request) 才能让请求流程继续。如果不调用它,请求就会在这里“卡住”,客户端将得不到响应。
  • async / await:
    • 由于 FastAPI 是异步框架,中间件函数也必须是异步的(async def),并且在调用 call_next 时必须使用 await

3. 中间件执行顺序:LIFO(后进先出)

中间件的执行顺序遵循后进先出(LIFO)的原则,即最后注册的中间件最先执行请求预处理,但最后执行响应后处理

@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

执行流程与输出

  1. 请求阶段(从外到内)
    • 请求首先到达 middleware_c (最后注册) -> 输出 "C: 请求前"
    • middleware_c 调用 call_next -> 请求进入 middleware_b
    • middleware_b -> 输出 "B: 请求前"
    • middleware_b 调用 call_next -> 请求进入 middleware_a
    • middleware_a -> 输出 "A: 请求前"
    • middleware_a 调用 call_next -> 请求到达路由函数 / 并执行。
  1. 响应阶段(从内到外)
    • 路由函数执行完毕,生成响应。
    • 响应返回给 middleware_a -> 执行 "A: 响应后" -> 输出 "A: 响应后"
    • 响应返回给 middleware_b -> 执行 "B: 响应后" -> 输出 "B: 响应后"
    • 响应返回给 middleware_c -> 执行 "C: 响应后" -> 输出 "C: 响应后"
    • 响应最终返回给客户端。

最终输出

C: 请求前
B: 请求前
A: 请求前
A: 响应后
B: 响应后
C: 响应后

重要提示:这个顺序意味着安全相关的中间件(如认证、CORS)通常应该尽早注册(即在 middleware_a 的位置),以便在请求流程的早期就进行拦截和验证。


三、常用内置中间件:开箱即用的解决方案

FastAPI 集成了几个非常实用的内置中间件,通过 app.add_middleware() 方法添加。

1. CORS 中间件(跨域资源共享)

解决浏览器同源策略(Same-Origin Policy)带来的跨域问题。

from fastapi.middleware.cors import CORSMiddleware

# 配置允许的源、方法、头等
app.add_middleware(
    CORSMiddleware,
    allow_origins=[  # 允许访问的前端域名列表
        "https://myfrontend.com",
        "http://localhost:3000",  # 开发环境
        "http://127.0.0.1:8080",
    ],
    allow_credentials=True,  # 是否允许发送 Cookies
    allow_methods=["*"],     # 允许的 HTTP 方法,["GET", "POST", "PUT", "DELETE"] 更安全
    allow_headers=["*"],     # 允许的请求头,["Authorization", "Content-Type"] 更安全
    # expose_headers=["X-Process-Time"], # 暴露给浏览器的自定义响应头
    # max_age=600, # 预检请求的结果缓存时间(秒)
)

@app.get("/")
async def main():
    return {"message": "Hello CORS World!"}

生产环境建议

  • 绝对避免 allow_origins=["*"],这会允许任何网站访问你的 API,存在严重的安全风险(如 CSRF 攻击)。
  • 尽可能精确地指定 allow_methodsallow_headers,遵循最小权限原则。

2. GZip 中间件(响应压缩)

自动压缩大于指定大小的响应体,显著减少传输数据量,提升性能。

from fastapi.middleware.gzip import GZipMiddleware

# 添加 GZip 中间件,只压缩大于 1000 字节的响应
app.add_middleware(GZipMiddleware, minimum_size=1000)

@app.get("/large-data")
async def large_response():
    # 返回一个大字符串,会被自动压缩
    return {"data": "x" * 2000}  # 2000 字符,大于 minimum_size

效果:客户端(浏览器或支持 gzip 的客户端)会收到一个经过 gzip 压缩的响应,其 Content-Encoding 响应头为 gzip。客户端需要解压后才能读取内容。

3. TrustedHost 中间件(防止 Host 头攻击)

防止攻击者通过伪造 Host 请求头进行缓存投毒、密码重置中毒等攻击。

from fastapi.middleware.trustedhost import TrustedHostMiddleware

app.add_middleware(
    TrustedHostMiddleware,
    allowed_hosts=[
        "myapi.com",           # 精确匹配
        "www.myapi.com",       # 精确匹配
        "*.myapi.com",         # 通配符,匹配所有子域名
        "localhost",           # 本地开发
        "127.0.0.1",
    ],
    # allowed_hosts=["*"] # 仅用于开发,生产环境禁用!
)

原理:如果请求的 Host 头不在 allowed_hosts 列表中,中间件会立即返回 400 Bad Request。

4. HTTPS 重定向中间件

强制将所有 HTTP 请求重定向到 HTTPS,确保通信安全。

from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware

# 添加 HTTPS 重定向中间件
app.add_middleware(HTTPSRedirectMiddleware)

# 现在,访问 http://myapi.com/ 会被 307 临时重定向到 https://myapi.com/

注意:此中间件应在生产环境中配合反向代理(如 Nginx)或云服务(如 AWS ALB)使用。直接在应用层处理 HTTPS 通常不是最佳实践。


四、自定义中间件实战:打造你的专属功能

1. 请求日志中间件(增强版)

记录详细的请求信息,是调试和监控的基础。

import time
import uuid
import logging
from fastapi import Request, Response
from typing import Callable

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("uvicorn.access")  # 使用 Uvicorn 的访问日志记录器

@app.middleware("http")
async def log_requests(request: Request, call_next: Callable):
    """
    记录每个请求的详细信息,包括唯一 ID、方法、URL、客户端 IP、处理时间和状态码。
    """
    # 为每个请求生成唯一 ID,便于追踪
    request_id = str(uuid.uuid4())

    # 获取客户端 IP
    client_ip = request.client.host if request.client else "unknown"

    # 记录请求开始
    logger.info(
        f"RID={request_id} | "
        f"IP={client_ip} | "
        f"REQ={request.method} {request.url.path} | "
        f"UA={request.headers.get('user-agent', 'unknown')[:50]}..."  # 截取过长的 UA
    )

    start_time = time.time()

    try:
        # 调用下一个处理环节
        response: Response = await call_next(request)

        # 计算处理时间
        process_time = time.time() - start_time

        # 记录请求完成
        logger.info(
            f"RID={request_id} | "
            f"RES={response.status_code} | "
            f"TIME={process_time:.3f}s"
        )

        return response

    except Exception as e:
        # 捕获并记录处理过程中的异常
        process_time = time.time() - start_time
        logger.error(
            f"RID={request_id} | "
            f"ERR=500 | "
            f"EXC={type(e).__name__}: {e} | "
            f"TIME={process_time:.3f}s"
        )
        # 重新抛出异常,让全局异常处理器处理
        raise

2. 认证中间件(简化版)

实现一个基于 Bearer Token 的全局认证。

from fastapi import Request, HTTPException, status
from typing import Callable

# 模拟的 token 验证(生产环境应使用 JWT、OAuth2 等)
VALID_TOKENS = {"secret-token-1", "secret-token-2"}

@app.middleware("http")
async def authenticate_request(request: Request, call_next: Callable):
    """
    全局认证中间件。检查除登录外所有端点的 Authorization 头。
    """
    # --- 白名单:跳过认证的路径 ---
    # 可以跳过 /login, /docs, /openapi.json 等
    whitelist_paths = ["/login", "/docs", "/openapi.json", "/redoc"]
    if request.url.path in whitelist_paths:
        return await call_next(request)

    # --- 检查 Authorization 头 ---
    auth_header = request.headers.get("Authorization")
    if not auth_header:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Authorization header missing",
            headers={"WWW-Authenticate": "Bearer"},
        )

    # 检查是否为 Bearer Token
    if not auth_header.startswith("Bearer "):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Authorization header must start with 'Bearer '",
            headers={"WWW-Authenticate": "Bearer"},
        )

    # 提取 token
    token = auth_header.split(" ", 1)[1]  # split max 1 time to handle spaces in token

    # --- 验证 token ---
    if token not in VALID_TOKENS:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired token",
            headers={"WWW-Authenticate": "Bearer"},
        )

    # ✅ 认证成功!可以将用户信息附加到 request 对象,供后续路由函数使用
    # request.state.user = get_user_from_token(token)  # 伪代码
    # 或者 request.state.token = token

    # 调用下一个处理环节
    response = await call_next(request)
    return response

注意:更灵活的认证通常使用依赖注入(Dependency)实现,可以更精细地控制哪些路由需要认证。

3. 限流中间件(内存版 - 仅作演示)

限制客户端的请求频率,防止滥用。

from collections import defaultdict
import time
from fastapi.responses import JSONResponse
from typing import Callable

# ⚠️ 警告:这是基于内存的简单实现,**不适合生产环境**!
# 生产环境应使用 Redis 或其他分布式存储。
# 因为在多进程/多服务器部署时,内存状态无法共享。
request_counts = defaultdict(list)  # key: client_ip, value: list of timestamps
RATE_LIMIT = 10  # 每分钟最多 10 次请求
RATE_LIMIT_PERIOD = 60  # 60 秒

@app.middleware("http")
async def rate_limit(request: Request, call_next: Callable):
    """
    简单的基于 IP 的限流中间件。
    """
    client_ip = request.client.host if request.client else "unknown"
    now = time.time()

    # --- 清理过期的请求记录 ---
    # 只保留最近 RATE_LIMIT_PERIOD 秒内的记录
    request_counts[client_ip] = [
        timestamp for timestamp in request_counts[client_ip]
        if now - timestamp < RATE_LIMIT_PERIOD
    ]

    # --- 检查是否超过速率限制 ---
    if len(request_counts[client_ip]) >= RATE_LIMIT:
        # ⛔ 触发限流
        return JSONResponse(
            status_code=429,  # Too Many Requests
            content={
                "detail": "请求过于频繁,请稍后再试。",
                "retry_after": RATE_LIMIT_PERIOD  # 建议客户端等待的时间(秒)
            },
            headers={"Retry-After": str(RATE_LIMIT_PERIOD)}  # 标准的 Retry-After 头
        )

    # --- 记录本次请求 ---
    request_counts[client_ip].append(now)

    # 调用下一个处理环节
    response = await call_next(request)
    return response

4. 统一响应格式中间件

强制所有 JSON 响应遵循一个统一的结构,便于前端处理。

import json
from fastapi.responses import JSONResponse
from typing import Callable

@app.middleware("http")
async def format_response(request: Request, call_next: Callable):
    """
    将所有 JSON 响应包装成统一的格式。
    原始响应体被视为 "data"。
    """
    response = await call_next(request)

    # 只处理 JSONResponse 类型的响应
    if isinstance(response, JSONResponse):
        try:
            # 读取原始响应体
            # body_iterator 是一个异步生成器,需要异步读取
            body = [chunk async for chunk in response.body_iterator]
            response_body = b"".join(body).decode()

            # 解析原始 JSON 数据
            data = json.loads(response_body)

            # 构建统一的响应格式
            unified_response = {
                "code": 0,  # 0 表示成功,非0表示错误
                "message": "Success",
                "data": data,
                "timestamp": int(time.time()),
                # "request_id": request_id # 如果有 request_id 可以加上
            }

            # 创建新的 JSONResponse 返回
            # 注意:需要复制原始响应的状态码
            return JSONResponse(
                content=unified_response,
                status_code=response.status_code,
                headers=dict(response.headers)  # 复制原始响应头
            )

        except (json.JSONDecodeError, UnicodeDecodeError) as e:
            # 如果原始响应不是有效的 JSON,保持原样返回
            print(f"Failed to parse response body as JSON: {e}")
            pass  # 继续执行最后的 return response
        except Exception as e:
            # 捕获其他意外错误
            print(f"Unexpected error in format_response middleware: {e}")
            pass

    # 对于非 JSON 响应(如 HTML, 文件下载)或解析失败的情况,保持原样
    return response

效果

  • 原始响应:{"message": "Hello"} (200 OK)
  • 经过中间件后:{"code": 0, "message": "Success", "data": {"message": "Hello"}, "timestamp": 1724071234} (200 OK)

五、第三方中间件:扩展功能边界

社区提供了丰富的第三方中间件来增强 FastAPI 的能力。

1. fastapi-limiter - 专业限流

基于 Redis 的强大限流库。

pip install fastapi-limiter aioredis
from fastapi import FastAPI, Request
from fastapi_limiter import FastAPILimiter
from fastapi_limiter.depends import RateLimiter
import aioredis

app = FastAPI()

@app.on_event("startup")
async def startup():
    """应用启动时初始化 Redis 连接和限流器。"""
    redis = aioredis.from_url("redis://localhost:6379", encoding="utf8", decode_responses=True)
    await FastAPILimiter.init(redis)

@app.on_event("shutdown")
async def shutdown():
    """应用关闭时关闭 Redis 连接。"""
    await FastAPILimiter.close()

@app.get("/limited")
@limiter.limit("5/minute")  # 使用装饰器限制:每分钟最多 5 次
async def limited(request: Request):
    return {"msg": "This is limited to 5 calls per minute"}

@app.get("/global-limited")
@limiter.limit("10/hour")  # 每小时最多 10 次
async def global_limited(request: Request):
    return {"msg": "Global rate limit"}

2. prometheus-fastapi-instrumentator - 监控指标

集成 Prometheus,暴露应用的监控指标。

pip install prometheus-fastapi-instrumentator
from prometheus_fastapi_instrumentator import Instrumentator

app = FastAPI()

# 在应用启动后初始化 Instrumentator
@app.on_event("startup")
async def startup():
    Instrumentator().instrument(app).expose(app)

# 现在访问 /metrics 端点即可获取 Prometheus 格式的指标
# 指标包括:请求计数、处理时间、活跃连接数等。

六、中间件最佳实践

实践

说明与建议

保持轻量与高效

中间件对每个请求都会执行,避免在其中进行阻塞的 I/O 操作(如同步数据库查询、文件读写)。优先使用异步操作。

健壮的错误处理

在中间件中使用 try...except

捕获异常。可以记录错误日志,并决定是返回自定义错误响应还是重新抛出异常。避免中间件自身崩溃导致整个应用不可用。

合理安排执行顺序

利用 LIFO 原则。通常顺序为:

-> HTTPSRedirect

-> TrustedHost

-> CORSMiddleware

-> 认证中间件

-> 日志中间件

-> 业务中间件

-> GZipMiddleware

。安全相关的应靠前。

谨慎使用全局状态

避免在中间件中使用全局变量存储请求特定的数据(如 request_id)。应使用 request.state(如 request.state.user)来附加数据,这是 FastAPI 推荐的方式。

性能监控

利用中间件测量请求处理时间(如 X-Process-Time

头),这是性能分析的基础。

安全第一

优先注册安全中间件(CORS, HTTPSRedirect, TrustedHost),并在认证中间件中严格验证。

日志记录

记录关键信息,但注意不要记录敏感数据(如密码、完整的请求体、token)。使用 request_id

追踪单个请求的完整生命周期。


七、中间件 vs 依赖注入:何时使用?

特性

中间件 (Middleware)

依赖注入 (Dependency Injection)

作用范围

全局。影响应用中的每一个请求。

灵活。可以应用于全局、特定路由、特定参数、特定类。

执行时机

固定在请求/响应生命周期的特定阶段(请求前、响应后)。

非常灵活。可以在函数执行前、参数解析时、类实例化时等。

复用性

高,但通常是全局性的。

极高。依赖可以像积木一样被组合、嵌套和复用。

测试性

较难单独测试,因为它与整个应用生命周期耦合。

易于单元测试。可以单独测试一个依赖函数,传入模拟的参数。

典型用途

横切关注点:日志、CORS、压缩、HTTPS 重定向、全局请求计时、全局异常捕获框架。

业务相关共享功能:数据库会话(get_db)、用户认证(get_current_user)、配置加载、特定业务逻辑的复用。

💡 核心建议

  • 使用中间件处理那些必须对所有请求生效的、与业务逻辑无关基础设施级功能。例如,你总是需要记录每个请求的日志,或者必须处理跨域问题。
  • 使用依赖注入处理那些可选的与特定业务逻辑紧密相关的、需要精细控制作用范围的功能。例如,只有 /users/ 相关的路由才需要数据库会话,只有 /admin/ 路由才需要管理员权限认证。

简单判断:如果一个功能需要“全局开启”,用中间件。如果一个功能是“按需注入”,用依赖注入。


八、总结:中间件是 API 的“骨架”与“神经系统”

中间件类型

核心用途

自定义中间件 (@app.middleware)

实现日志、性能监控、统一响应、自定义认证/限流等。

CORS 中间件

解决浏览器跨域问题,安全地允许特定源访问 API。

GZip 中间件

自动压缩响应,优化网络传输性能。

TrustedHost 中间件

防止 Host 头攻击,确保请求来源合法。

HTTPSRedirect 中间件

强制使用 HTTPS,保障通信安全。

第三方中间件

扩展功能,如 fastapi-limiter(限流), prometheus-fastapi-instrumentator (监控)。

🚀 实践建议

  1. 从基础做起:为你的 FastAPI 应用标配 CORSMiddleware (按需)、日志中间件和处理时间中间件。
  2. 安全优先:根据部署环境,考虑是否需要 TrustedHostMiddlewareHTTPSRedirectMiddleware
  3. 性能优化:对于返回大量数据的 API,GZipMiddleware 是简单有效的优化手段。
  4. 善用社区:不要重复造轮子,积极寻找和使用成熟的第三方中间件。
  5. 理解差异:清晰区分中间件和依赖注入的应用场景,选择最合适的工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值