中间件 Middleware 与 CORS
本教程共 50 篇 · 第 41 篇 · 更新于 2026-08-12 · 约 7 分钟阅读
本节目标:学会写中间件去统一拦截请求和响应,并搞懂前后端分离时浏览器报的跨域错误该怎么用 CORS 解决。
有些逻辑希望”对所有请求都生效”,比如统计每个接口花了多少时间、给所有响应加个统一头。这种横切逻辑不该写进每个接口里。中间件就是干这个的。另外,只要你的前端和后端不在同一个地址上,浏览器就会用 CORS 规则拦你。这两件事常常一起出现,所以放一章讲。
41-1 什么是中间件
中间件是一个函数,它对每一个进来的请求、每一个出去的响应都起作用。它干的事可以分成几步:
- 收到请求,在交给具体接口之前,可以先看一眼、先改一改。
- 把请求交给后面的路径操作函数处理。
- 拿到响应,在返回给客户端之前,再加工一下。
- 把响应返回。
想象成小区的门卫:每个访客进门他都要登记,出门时他再检查一遍。所有请求都得过他这关,所以适合做统一的、全局的事。
41-2 写一个中间件
在 FastAPI 里用 @app.middleware("http") 装饰一个函数,它就成中间件了。这个函数固定收两个参数:一个是 request,另一个是 call_next。call_next 负责把请求交给后面的接口,并返回响应。
import time
from fastapi import FastAPI, Request
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
这段代码的流程是:记下开始时间,用 await call_next(request) 把请求交给接口拿到响应,算出耗时,然后往响应头里塞一个 X-Process-Time。浏览器或调用方就能在响应头里看到这次处理花了几秒。
Tip这里用
time.perf_counter()而不是time.time(),因为它精度更高,更适合量这种短时间。注意call_next前面要加await,因为它本身是异步的。
41-3 在请求前和响应后都能动手
中间件最灵活的地方:你在 call_next 之前写的代码,是在接口处理请求之前运行的;在 call_next 之后写的代码,是在拿到响应、还没返回之前运行的。
比如你想在请求进来时打印日志,再在响应里加头:
@app.middleware("http")
async def log_requests(request: Request, call_next):
print(f"收到请求:{request.method} {request.url.path}")
response = await call_next(request)
response.headers["X-Powered-By"] = "FastAPI"
return response
请求前进门打印一行,响应后加个头,两不耽误。
Note自定义响应头如果想让浏览器里的 JavaScript 读到,得在 CORS 的
expose_headers里登记(见下文)。否则浏览器出于安全会把它藏起来。
41-4 多个中间件的执行顺序
你可以叠好几个中间件。它们像套娃一样包着应用:后加的是最外层,先加的是最内层。请求时从最外层进,响应时从最内层出。
# MiddlewareA、MiddlewareB 仅为示意,实际传入你要用的中间件类本身
app.add_middleware(MiddlewareA)
app.add_middleware(MiddlewareB)
执行顺序是:请求走 MiddlewareB → MiddlewareA → 接口;响应走 接口 → MiddlewareA → MiddlewareB。记住”后进先出”这个口诀,排错时很有用。
41-5 跨域 CORS 到底在拦什么
现在说 CORS。当前端(浏览器里跑的 JavaScript)去请求一个”源”不同的后端时,浏览器会按 CORS 规则把关。所谓”源”,是协议 + 域名 + 端口三件套。只要有一项不同,就是不同源。
举例,下面这些都是不同的源:
http://localhosthttps://localhosthttp://localhost:8080
哪怕都在 localhost,协议或端口不同,浏览器就认为是跨域。假设你的前端在 http://localhost:8080,后端在 http://localhost(默认 80 端口),前端发请求就会被拦。
浏览器会先发一个 OPTIONS 预检请求探口风。后端如果返回”允许这个源”的响应头,浏览器才放行真正的请求。所以后端必须有一份”允许的源”清单。
41-6 用 CORSMiddleware 放开跨域
FastAPI 自带 CORSMiddleware,配置起来很直接:导入它,列一份允许源清单,再用 app.add_middleware 挂上。
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
origins = [
"http://localhost.tiangolo.com",
"https://localhost.tiangolo.com",
"http://localhost",
"http://localhost:8080",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
这样,清单里的源就能正常调你的接口了。allow_methods=["*"] 表示放行所有 HTTP 方法,allow_headers=["*"] 表示放行所有请求头。
41-7 常用配置项说明
CORSMiddleware 支持这些重要参数:
allow_origins:允许跨域的源列表,如["https://example.org"]。写["*"]表示放行全部。allow_origin_regex:用正则匹配源,比如'https://.*\.example\.org'。allow_methods:允许的 HTTP 方法,默认只放行GET。写["*"]放行全部。allow_headers:允许的请求头,默认空。写["*"]放行全部。allow_credentials:是否允许带凭证(Cookie、Authorization 头)。默认False。expose_headers:允许浏览器前端读到的响应头,默认空。max_age:浏览器缓存 CORS 结果的秒数,默认 600。
Warning一旦
allow_credentials=True,allow_origins就不能用["*"],必须明确列出具体源,这是浏览器的硬规定。注意限制只针对 origins:allow_methods=["*"]、allow_headers=["*"]仍可用,Starlette 会按实际请求回显对应的方法和头。
41-8 前后端分离的真实场景
前后端分离是最常见的踩坑现场。比如前端跑在 http://localhost:5173(Vite 开发服务器),后端跑在 http://localhost:8000。两者端口不同,必然跨域。后端清单里加上前端那个源即可:
origins = [
"http://localhost:5173",
"https://my-frontend.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
开发阶段图省事,有人会写 allow_origins=["*"]。但要注意:带了凭证就不能用通配符。生产环境请务必写清具体源,别偷懒,否则会有安全风险。
CORS 中间件只处理两类请求:带 Origin 头的简单请求,以及带 Origin 和 Access-Control-Request-Method 的 OPTIONS 预检请求。预检通过了,真正的请求才放行。
41-9 小结
这一章我们打通了全局拦截和跨域:
- 中间件用
@app.middleware("http"),靠call_next串起请求和响应。 - 计时、加头、记日志这类全局逻辑都适合放中间件里。
- 多个中间件”后进先出”,请求从外进、响应从外回。
- CORS 是浏览器对跨域请求的安全限制,用
CORSMiddleware配置允许的源、方法、头来解决。 - 带凭证时不能用
*通配符,必须列具体源。
下一章我们讲自定义响应,比如直接返回 HTML、文件,或者流式地一点点吐数据。