一句話先說結論: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不能共存,二選一。- 生產環境列具體源,別圖省事上通配符。
