Skip to content

中间件与 CORS ​

中间件像一条统一处理通道 ​

中间件可以在请求进入路由前执行工作,也可以在拿到响应后增加信息。适合统一计时、请求标识、日志等横跨多个接口的处理。官方中间件说明

text
请求 → 中间件前半段 → 路由及其依赖 → 中间件后半段 → 响应

这是帮助入门的简化流程。异常处理、带 yield 的依赖清理、流式传输和后台任务还有各自的执行阶段。

为什么需要“横跨多个接口”的统一处理? 如果每个接口里都自己写一遍“记录开始时间、算耗时、写日志”,同样的代码会重复几十遍;而中间件能让这些逻辑只写一次,自动对所有请求生效。就像小区门口的一道门禁,不管你去哪栋楼,都要先经过它。

多个中间件怎么办:像剥洋葱一样进出 ​

一个应用往往不止一个中间件。比如你可能同时需要:记录日志、统计耗时、校验请求头、处理 CORS。这时多个中间件会层层嵌套,就像剥洋葱:

text
请求 → 中间件①前半段 → 中间件②前半段 → 路由 → 中间件②后半段 → 中间件①后半段 → 响应

中间件①在最外层,中间件②在最里层。请求进来时,先经过外层①,再经过内层②;拿到响应返回时,顺序反过来,先经过内层②,再经过外层①。业内把这个结构叫洋葱模型(Onion Model)。

生活化类比:想象去一家公司办业务——

  1. 进门先过公司大门安检(外层①);
  2. 再到具体部门前台登记(内层②);
  3. 办完业务(路由);
  4. 出部门时再登记一次(内层②的后半段);
  5. 出大门时再过一次安检(外层①的后半段)。

进去和出来的顺序正好相反。

执行顺序:后注册的先执行 ​

中间件谁是外层、谁是内层,取决于注册顺序。FastAPI(底层是 Starlette)的规则是:后注册的中间件更靠外,先执行,也就是“后进先出”。

用打印来看最直观:

python
from fastapi import FastAPI, Request

app = FastAPI()


@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.get("/hello")
def hello():
    print("路由:处理中")
    return {"message": "hello"}

访问 /hello 时,控制台打印顺序是:

text
B:请求进来
A:请求进来
路由:处理中
A:响应出去
B:响应出去

middleware_b 写在后面(后注册),所以它更靠外、先执行;middleware_a 写在前面(先注册),反而更靠里、后执行。

@app.middleware("http") 装饰器的注册顺序和 app.add_middleware(...) 的调用顺序遵循同一规则:越靠后注册,越靠外执行。第一次接触时容易搞反,建议用上面的打印实验亲自验证一次。

顺序会带来什么影响 ​

既然顺序重要,那该怎么安排?两个常见原则:

  1. 需要“包住”其他逻辑的中间件放后面注册。 比如 CORS 通常放在最外层,这样浏览器的预检请求(OPTIONS)能最先被拦下处理,不必进入业务逻辑。
  2. 顺序不同,结果可能不同。 例如“统计耗时”如果放在最外层,它能算出包含其他中间件在内的总耗时;如果放在最里层,就只能算出路由处理本身的耗时。想要哪种口径,就把它放在对应位置。

CORS 是浏览器的跨域访问规则 ​

假设前端运行在 http://localhost:5173,后端运行在 http://localhost:8000,端口不同,所以它们属于不同源。

“源”由协议、主机和端口共同决定;localhost 与 127.0.0.1 也不能随意当成同一个允许来源。官方 CORS 说明

完整示例:计时与跨域 ​

python
import time
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
    expose_headers=["X-Process-Time"],
)


@app.middleware("http")
async def add_process_time(request: Request, call_next):
    started = time.perf_counter()
    response = await call_next(request)
    elapsed = time.perf_counter() - started
    response.headers["X-Process-Time"] = f"{elapsed:.6f}"
    return response


@app.get("/health")
def health():
    return {"status": "ok"}

call_next(request) 将请求交给后续处理;拿到响应后添加计时头。这里测量的是取得响应对象前的耗时,不一定包含整个流式响应传输时间。

这个例子里 CORSMiddleware 用 add_middleware 先注册,计时中间件用装饰器后注册,所以计时中间件更靠外。要不要让 CORS 更靠外,取决于你希望预检请求在计时前还是计时后处理——大多数场景让 CORS 靠外更稳妥。

CORS 配置项怎么理解 ​

配置项含义
allow_origins哪些前端来源允许跨域访问
allow_credentials是否允许浏览器携带凭据,例如 Cookie
allow_methods允许哪些方法
allow_headers允许客户端发送哪些请求头
expose_headers哪些响应头允许前端 JavaScript 读取

需要凭据时,采用明确来源列表,并明确配置方法和请求头,不要简单地把所有参数都设为 "*"。官方凭据与通配符说明

为什么其他工具能访问,浏览器却报错 ​

CORS 主要由浏览器执行,因此 /docs、Python 客户端或其他请求工具能调用成功,不代表前端跨域调用一定成功。

某些跨域请求会先发送 OPTIONS 预检,询问服务器是否允许这次请求。请检查浏览器网络面板中的预检响应、来源、方法和请求头。

CORS 不是权限认证。 即使一个来源没被允许,其他非浏览器客户端仍可能发送请求。接口是否允许访问,应由后端认证和授权逻辑判断。

本章练习 ​

  1. 观察 /health 响应头中的计时;比较来源 http://localhost:5173 与 http://localhost:3000 的跨域响应。
  2. 注册两个打印中间件,先手写你预测的打印顺序,再访问接口验证是否一致。
  3. 把计时中间件从最外层移到最里层,比较两次测出的耗时有什么不同。

过关标准:能区分跨域问题、接口错误、网络错误和未登录错误;能说清多个中间件的执行顺序。