中间件与 CORS
中间件像一条统一处理通道
中间件可以在请求进入路由前执行工作,也可以在拿到响应后增加信息。适合统一计时、请求标识、日志等横跨多个接口的处理。官方中间件说明
请求 → 中间件前半段 → 路由及其依赖 → 中间件后半段 → 响应这是帮助入门的简化流程。异常处理、带 yield 的依赖清理、流式传输和后台任务还有各自的执行阶段。
为什么需要“横跨多个接口”的统一处理? 如果每个接口里都自己写一遍“记录开始时间、算耗时、写日志”,同样的代码会重复几十遍;而中间件能让这些逻辑只写一次,自动对所有请求生效。就像小区门口的一道门禁,不管你去哪栋楼,都要先经过它。
多个中间件怎么办:像剥洋葱一样进出
一个应用往往不止一个中间件。比如你可能同时需要:记录日志、统计耗时、校验请求头、处理 CORS。这时多个中间件会层层嵌套,就像剥洋葱:
请求 → 中间件①前半段 → 中间件②前半段 → 路由 → 中间件②后半段 → 中间件①后半段 → 响应中间件①在最外层,中间件②在最里层。请求进来时,先经过外层①,再经过内层②;拿到响应返回时,顺序反过来,先经过内层②,再经过外层①。业内把这个结构叫洋葱模型(Onion Model)。
生活化类比:想象去一家公司办业务——
- 进门先过公司大门安检(外层①);
- 再到具体部门前台登记(内层②);
- 办完业务(路由);
- 出部门时再登记一次(内层②的后半段);
- 出大门时再过一次安检(外层①的后半段)。
进去和出来的顺序正好相反。
执行顺序:后注册的先执行
中间件谁是外层、谁是内层,取决于注册顺序。FastAPI(底层是 Starlette)的规则是:后注册的中间件更靠外,先执行,也就是“后进先出”。
用打印来看最直观:
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 时,控制台打印顺序是:
B:请求进来
A:请求进来
路由:处理中
A:响应出去
B:响应出去middleware_b 写在后面(后注册),所以它更靠外、先执行;middleware_a 写在前面(先注册),反而更靠里、后执行。
@app.middleware("http")装饰器的注册顺序和app.add_middleware(...)的调用顺序遵循同一规则:越靠后注册,越靠外执行。第一次接触时容易搞反,建议用上面的打印实验亲自验证一次。
顺序会带来什么影响
既然顺序重要,那该怎么安排?两个常见原则:
- 需要“包住”其他逻辑的中间件放后面注册。 比如 CORS 通常放在最外层,这样浏览器的预检请求(
OPTIONS)能最先被拦下处理,不必进入业务逻辑。 - 顺序不同,结果可能不同。 例如“统计耗时”如果放在最外层,它能算出包含其他中间件在内的总耗时;如果放在最里层,就只能算出路由处理本身的耗时。想要哪种口径,就把它放在对应位置。
CORS 是浏览器的跨域访问规则
假设前端运行在 http://localhost:5173,后端运行在 http://localhost:8000,端口不同,所以它们属于不同源。
“源”由协议、主机和端口共同决定;localhost 与 127.0.0.1 也不能随意当成同一个允许来源。官方 CORS 说明
完整示例:计时与跨域
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 不是权限认证。 即使一个来源没被允许,其他非浏览器客户端仍可能发送请求。接口是否允许访问,应由后端认证和授权逻辑判断。
本章练习
- 观察
/health响应头中的计时;比较来源http://localhost:5173与http://localhost:3000的跨域响应。 - 注册两个打印中间件,先手写你预测的打印顺序,再访问接口验证是否一致。
- 把计时中间件从最外层移到最里层,比较两次测出的耗时有什么不同。
过关标准:能区分跨域问题、接口错误、网络错误和未登录错误;能说清多个中间件的执行顺序。