Appearance
中间件与 CORS
前端 06 篇提到过 CORS——浏览器发现前端和后端端口不一样,就把响应拦住了。解决方法在后端配 CORS 中间件。中间件是 FastAPI 里"请求进来和出去都经过的一层",CORS 只是其中一种。
一、中间件是什么
中间件是包在路由外面的一层——每个请求进来先经过中间件,路由处理完返回响应时再经过中间件出去。
可以理解为高速公路上的收费站——请求进来过一道,响应出去再过一道。每个请求和响应都会经过所有注册的中间件。CORS、日志、限流、请求计时,这些"每个请求都要做的事"都适合写成中间件。
二、CORS 中间件
前端 localhost:5173 调后端 localhost:8000,浏览器报错 CORS policy: No 'Access-Control-Allow-Origin。原因是浏览器同源策略:不同端口算跨域,后端没明确说"允许这个来源",浏览器就拦住响应。
python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173", # 前端开发服务器
"http://localhost:3000", # 备用端口
],
allow_credentials=True, # 允许带 Cookie
allow_methods=["*"], # 允许所有 HTTP 方法
allow_headers=["*"], # 允许所有请求头
)加了这段,后端响应里会带上 Access-Control-Allow-Origin: http://localhost:5173 头。浏览器看到这个头,确认后端允许了,就放行响应。
生产环境收紧来源
allow_origins=["*"] 允许任何来源——开发时方便,生产环境绝对不能用。生产环境要写明确的域名列表:
python
allow_origins=[
"https://ops.example.com", # 生产前端地址
"https://staging.example.com", # 预发布环境
]预检请求(OPTIONS)
浏览器对"非简单请求"(比如 POST JSON、带自定义头的请求)会先发一个 OPTIONS 请求问后端:"我能不能用这个方法和这些头发请求?"后端回复允许的方法和头,浏览器才发真正的请求。
CORSMiddleware 自动处理 OPTIONS 预检请求——不用自己写 OPTIONS 路由。浏览器 Network 面板里看到 OPTIONS 请求返回 200,就是预检通过了。
三、自定义中间件——请求计时
写一个中间件记录每个请求的耗时:
python
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def log_request_time(request: Request, call_next):
"""记录每个请求的处理时间。"""
start = time.time()
response = await call_next(request) # 继续往下走,到路由函数
duration = time.time() - start
response.headers["X-Response-Time"] = f"{duration:.3f}s"
print(f"{request.method} {request.url.path} → {response.status_code} ({duration:.3f}s)")
return responsecall_next(request) 是关键——它把请求传给下一层(最终到路由函数),拿到响应后再继续往下执行。中间件里 call_next 前面的代码在请求进来时执行,后面的代码在响应出去时执行。所以 start = time.time() 在请求前记录,duration = time.time() - start 在响应后算差值。
四、中间件的执行顺序
多个中间件按注册顺序执行,进来时先注册的先执行,出去时后注册的先执行(洋葱模型):
python
app.add_middleware(MiddlewareA) # 第三注册
app.add_middleware(MiddlewareB) # 第二注册
app.add_middleware(MiddlewareC) # 第一注册
# 请求进来:C → B → A → 路由
# 响应出去:A → B → C实际使用时这个顺序通常不敏感,但写需要精确控制的中间件(比如认证中间件要在日志中间件之前)时要留意。
五、依赖注入回顾
前面几篇大量用了 Depends——数据库会话、当前用户。依赖注入跟中间件的区别:
| 中间件 | 依赖注入 | |
|---|---|---|
| 作用范围 | 所有请求都经过 | 特定接口才触发 |
| 典型场景 | CORS、日志、限流 | 数据库连接、认证、权限 |
| 能否拿到路由参数 | 不能 | 能(函数参数) |
认证放在依赖注入而不是中间件里,就是因为不是所有接口都需要登录——/login 接口匿名访问,/articles 接口需要登录。依赖注入是逐接口声明的,中间件是全局的。
依赖嵌套
依赖可以嵌套——get_current_user 依赖 oauth2_scheme(提取 Token),get_admin_user 依赖 get_current_user(并检查是不是管理员):
python
async def get_current_user(token: str = Depends(oauth2_scheme)):
# 解析 Token,返回用户
return {"id": 1, "username": "admin", "role": "admin"}
async def get_admin_user(user: dict = Depends(get_current_user)):
if user["role"] != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return user
@app.delete("/articles/{article_id}")
def delete_article(article_id: int, admin: dict = Depends(get_admin_user)):
# 只有管理员能调这个接口
return {"deleted": article_id}get_admin_user 自动先执行 get_current_user(验证 Token),再检查角色。FastAPI 自动解析依赖链,不用手动嵌套调用。