一句话先说结论:CORS 是浏览器执行的策略,后端要做的是“把正确的响应头带上”。FastAPI 默认不加任何 CORS 头,必须显式加
CORSMiddleware。最常见的坑是:没加中间件、allow_origins漏了前端源、以及allow_origins=["*"]和allow_credentials=True撞在一起——这个组合浏览器会直接拒掉,而且服务端一点不报错。
背景
前端跑在 http://localhost:3000,后端跑在 http://localhost:8000,两者端口不同就是跨域。浏览器的同源策略会拦住响应,除非后端在响应头里明确放行。现代 REST API 几乎都会触发预检(OPTIONS):只要带了 Authorization 头、用了 application/json、或用了 PUT/DELETE,浏览器都会先发一个 OPTIONS 问一句“允不允许”。
这也解释了为什么 curl/Postman 一切正常、浏览器却报错——curl 不执行 CORS。
现象
前端控制台:
Access to fetch at 'http://localhost:8000/api/data'
from origin 'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
更隐蔽的一种,当 allow_origins=["*"] 同时开了 allow_credentials=True 时:
The value of the 'Access-Control-Allow-Origin' header in the response
must not be the wildcard '*' when the request's credentials mode is 'include'.
根因分析
FastAPI 的 CORSMiddleware 默认是最保守的,几个参数不填会踩坑:
- 没加中间件:压根不发任何 CORS 头。
allow_origins漏源:列表里没写前端源,Access-Control-Allow-Origin就对不上。allow_methods默认只有['GET']:如果你只写allow_origins不写 methods,POST/PUT 会被预检拦下——很多人不知道这个默认值。allow_headers默认空列表:前端带Authorization这样的自定义头,必须显式列入,否则预检失败。allow_origins=["*"]+allow_credentials=True:这是被 Fetch 规范明令禁止的组合。浏览器要求“带凭据”时Access-Control-Allow-Origin必须是具体源、不能是*。这个错服务端不会报错,allow_credentials被静默忽略,排查起来特别费劲(FastAPI 0.100+ 会在启动时抛ValueError,旧版本则静默放行,反而埋下 CSRF 隐患)。
解决方案
生产:具体 origin + 凭据
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://app.example.com",
"https://admin.example.com",
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization"],
expose_headers=["X-Total-Count"],
max_age=3600,
)
开发 / 公开只读 API:通配符,但别带凭据
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 通配符 = 不带凭据
allow_credentials=False, # 必须 False
allow_methods=["*"],
allow_headers=["*"],
)
两条路只能二选一:想带 cookie/token,就列具体源;想通配,就别指望凭据。
环境变量管理源列表,注意 strip
用环境变量注入允许源时,记得去空格,否则 "a, b" 里的 b 因为带前导空格而匹配不上:
import os
allowed = [o.strip() for o in os.getenv("ALLOWED_ORIGINS", "").split(",") if o.strip()]
if "*" in allowed and len(allowed) > 1:
raise ValueError("不能把通配符 * 和具体源混在一起")
app.add_middleware(
CORSMiddleware,
allow_origins=allowed,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization"],
)
排查小抄
- 先看响应头有没有
Access-Control-Allow-Origin,没有就是中间件没生效或没加。 - 再看
allow_origins列表是否精确包含前端源(协议、域名、端口一个都不能差)。 - 检查
allow_methods/allow_headers是否漏了前端实际用到的方法和头。 - 带 cookie/token 时,确认没用
["*"]。 - 用
TestClient跑一遍预检请求,能当场抓住这类问题:
from fastapi.testclient import TestClient
client = TestClient(app)
r = client.options("/api/data", headers={
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "GET",
})
assert "access-control-allow-origin" in r.headers
小结
- FastAPI 默认无 CORS,必须显式
add_middleware(CORSMiddleware, ...)。 allow_methods默认只有 GET、allow_headers默认空,别漏配。allow_origins=["*"]和allow_credentials=True不能共存,二选一。- 生产环境列具体源,别图省事上通配符。