中间件(Middleware)应用:CORS 跨域处理、GZip 压缩与请求耗时统计

📂 所属阶段:第二阶段 — 进阶黑科技(核心篇)
🔗 相关章节:异步编程深度解析 · 异常处理


1. 中间件是什么?

1.1 中间件在请求生命周期中的位置

请求到达

┌─────────────────────────────────────────┐
│  中间件 1(记录日志)                      │
│     ↓                                   │
│  中间件 2(CORS 处理)                     │
│     ↓                                   │
│  中间件 3(GZip 压缩)                     │
│     ↓                                   │
│  路由处理函数 ──── 返回响应 ────→ 反向经过所有中间件 │
└─────────────────────────────────────────┘

中间件是一个"包裹"着应用的函数,请求和响应都会经过它。你可以把它理解为请求的"安检通道"和"包装工厂"。

1.2 FastAPI 中间件 vs 依赖注入

中间件依赖注入
触发时机每个请求都自动经过按需注入到路由参数
执行顺序按注册顺序(先注册先执行)与路由函数参数对应
用途全局拦截、日志、跨域、压缩提取参数、认证、数据库
无法获取路由函数的返回值无法拦截全局请求

2. 自定义中间件

2.1 基础语法

from fastapi import FastAPI, Request
import time

app = FastAPI()

# 自定义中间件
@app.middleware("http")
async def middleware_name(request: Request, call_next):
    # ── 请求前处理 ──
    print(f"收到请求: {request.url.path}")

    # call_next 传递请求到下一个中间件或路由
    response = await call_next(request)

    # ── 响应后处理 ──
    print(f"响应状态: {response.status_code}")

    return response

2.2 请求耗时统计中间件

@app.middleware("http")
async def add_process_time(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(round(process_time * 1000, 2)) + "ms"

    return response

2.3 日志记录中间件

import logging
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@app.middleware("http")
async def log_requests(request: Request, call_next):
    # 请求前
    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

3. CORS 跨域中间件

3.1 为什么需要 CORS?

浏览器出于安全考虑,默认禁止一个域名(如 https://app.example.com)的网页向另一个域名(如 https://api.example.com)发请求。CORS(Cross-Origin Resource Sharing)就是让服务器声明"允许哪些外域来访问我"。

3.2 FastAPI 内置 CORS 中间件

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

app = FastAPI()

# 配置 CORS 中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "https://daomanpy.com",        # 允许特定域名
        "https://www.daomanpy.com",
        "http://localhost:3000",       # 开发环境
    ],
    allow_credentials=True,            # 允许携带 Cookie
    allow_methods=["*"],               # 允许所有方法,或指定 ["GET", "POST"]
    allow_headers=["*"],              # 允许所有请求头,或指定 ["Authorization"]
)

# 所有路由自动支持 CORS
@app.get("/api/data")
async def get_data():
    return {"hello": "跨域成功!"}

3.3 不同场景的 CORS 配置

# 场景一:允许所有来源(仅开发环境使用)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 场景二:生产环境(推荐,明确指定域名)
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "https://daomanpy.com",
        "https://www.daomanpy.com",
    ],
    allow_credentials=True,
    allow_methods=["GET", "POST"],
    allow_headers=["Authorization", "Content-Type"],
)

# 场景三:允许携带 Cookie(必须指定具体 origins,不能用 *)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://daomanpy.com"],
    allow_credentials=True,  # 必须为 True
    allow_methods=["*"],
    allow_headers=["*"],
    expose_headers=["X-Request-ID"],  # 暴露给前端的响应头
    max_age=600,                       # 预检请求缓存时间(秒)
)

4. GZip 压缩中间件

4.1 Starlette 内置 GZip 中间件

from fastapi import FastAPI
from starlette.middleware.gzip import GZipMiddleware

app = FastAPI()
app.add_middleware(GZipMiddleware, minimum_size=1000)
  • minimum_size:响应体大于此字节数才压缩(默认 500 字节)
  • 自动对文本