Skip to content

中间件与 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 response

call_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 自动解析依赖链,不用手动嵌套调用。