前后端分离开发时,浏览器控制台突然冒出一行红字:”Access to fetch at ‘http://localhost:8000/api‘ from origin ‘http://localhost:3000‘ has been blocked by CORS policy”。这是几乎所有 Web 开发者都会遇到的经典跨域报错。FastAPI 通过内置的 CORSMiddleware 优雅地解决了这个问题,但理解其背后的机制才能避免”一把梭允许所有来源”的安全隐患。

什么是 CORS

CORS(Cross-Origin Resource Sharing,跨域资源共享)是浏览器的一种安全策略。当网页发起 HTTP 请求时,如果请求的目标地址与当前页面的协议、域名、端口任意一项不同,就属于跨域请求,浏览器会先检查服务器是否明确允许。

判断是否同源的三要素:

当前页面 请求目标 是否跨域
http://a.com http://a.com/api ❌ 同源
http://a.com https://a.com ✅ 协议不同
http://a.com http://b.com ✅ 域名不同
http://a.com:3000 http://a.com:8000 ✅ 端口不同

本地开发中,前端(如 Vite 的 localhost:5173)和后端(如 FastAPI 的 localhost:8000)端口不同,自然触发跨域检查。

FastAPI 中的 CORS 配置

FastAPI 通过 CORSMiddleware 中间件来处理跨域,核心配置只有几行:

1
2
3
4
5
6
7
8
9
10
11
12
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"], # 允许的来源
allow_credentials=True, # 允许携带 Cookie
allow_methods=["GET", "POST", "PUT", "DELETE"], # 允许的 HTTP 方法
allow_headers=["Content-Type", "Authorization"], # 允许的请求头
)

关键参数说明

**allow_origins**:最重要的参数,指定哪些前端域名可以访问你的 API。必须精确匹配,http://localhosthttps://localhost 被视为不同的源。开发阶段常用 ["*"] 允许所有来源,但生产环境必须限制为具体的域名列表。

**allow_credentials:是否允许请求携带 Cookie 和 HTTP 认证信息。当设为 True 时,allow_origins 不能为 ["*"]**,必须指定具体域名,这是浏览器的强制安全限制。

**allow_methods**:允许的 HTTP 方法。默认只允许 GET,如果你的 API 涉及 POST、PUT、DELETE 等操作,必须显式声明。

**allow_headers**:允许客户端发送的请求头。前后端分离中常见的 AuthorizationContent-Type 等需要在此声明。

允许所有来源(仅限开发/公开 API)

1
2
3
4
5
6
7
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False, # 必须为 False
allow_methods=["*"],
allow_headers=["*"],
)

允许所有来源时 allow_credentials 必须是 False,否则浏览器会拒绝请求。对于无需身份认证的公开 API,这个配置是合理的。

预检请求(Preflight Request)

当请求不是”简单请求”时,浏览器会先自动发送一个 OPTIONS 请求来”探路”,询问服务器是否允许即将发起的请求。这个过程叫预检请求。

以下情况会触发预检:

  • 使用 PUTDELETEPATCH 等方法
  • Content-Type 不是 application/x-www-form-urlencodedmultipart/form-datatext/plain
  • 使用了自定义请求头(如 AuthorizationX-Requested-With
1
2
3
4
5
6
7
8
9
# 这个请求会触发预检:PUT 方法 + JSON + Authorization 头
fetch("http://localhost:8000/api/users/1", {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer xxx"
},
body: JSON.stringify({ name: "Alice" })
})

CORSMiddleware 自动处理 OPTIONS 请求并返回正确的响应头,开发者无需手动编写 OPTIONS 路由。

实际案例:前后端分离项目配置

假设项目结构如下——前端 React 跑在 localhost:3000,后端 FastAPI 跑在 localhost:8000,需要携带 Cookie 做身份认证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# 根据环境动态配置
import os

if os.getenv("ENV") == "production":
origins = ["https://www.myapp.com", "https://admin.myapp.com"]
else:
origins = ["http://localhost:3000", "http://127.0.0.1:3000"]

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=["Content-Type", "Authorization"],
expose_headers=["X-Total-Count"], # 让前端能读取自定义响应头
)

@app.get("/api/me")
def get_current_user():
return {"username": "admin", "role": "developer"}

expose_headers 参数值得一提:默认情况下,浏览器只允许 JavaScript 读取响应的 Cache-ControlContent-Type 等 7 个简单头。如果你的后端在返回自定义头(如分页的 X-Total-Count),必须通过 expose_headers 暴露给前端。

常见问题排查

1. 配置了 CORS 但仍报跨域错误

首先检查 allow_origins 是否包含了完整的 origin(含协议和端口)。用浏览器开发者工具的 Network 面板查看响应头中是否有 Access-Control-Allow-Origin,如果返回了但值不对,通常就是 origin 拼写问题。

2. Cookie 无法传递

确认 allow_credentials=True 且前端请求也开启了 credentials: 'include'

1
fetch("/api/me", { credentials: "include" })

同时检查 allow_origins 是具体域名而非 ["*"]

3. 预检请求返回 405

检查 allow_methods 是否包含目标 HTTP 方法,以及 allow_headers 是否包含自定义请求头。

总结

FastAPI 的 CORS 配置本质上是对浏览器安全策略的声明——告诉浏览器哪些来源可以访问资源。核心记住三点:allow_origins 精确匹配来源、allow_credentials 为 True 时必须用具体域名、非简单请求会自动触发 OPTIONS 预检。开发阶段可以用宽松配置快速验证,但上线前务必收紧为具体的域名列表。