Skip to content

26. FastAPI 跨域资源共享与静态资源 ​

第一部分:跨域资源共享(CORS) ​

跨域是浏览器的来源检查 ​

来源由协议、主机和端口组成。http://localhost:5173 与 http://localhost:8000 端口不同,属于不同来源;localhost 与 127.0.0.1 主机也不同。浏览器脚本跨来源读取响应时,会应用 CORS 规则。

CORS 是浏览器约束,不是接口认证。命令行客户端和其他服务器仍可直接发请求,因此不能用允许来源列表代替访问权限校验。

预检请求是什么 ​

跨域发送 JSON、自定义头或非简单方法时,浏览器可能先发 OPTIONS,询问目标服务是否允许。中间件检查 Origin、Access-Control-Request-Method 等头并回应,通过后浏览器再发真正业务请求。

在 PowerShell 中可模拟检查:

powershell
curl.exe -i -X OPTIONS http://127.0.0.1:8000/items -H "Origin: http://localhost:5173" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: authorization"

检查 Access-Control-Allow-Origin 是否精确等于允许的来源。普通 curl 请求成功不代表浏览器跨域读取一定成功。

全局级跨域 ​

全局级跨域用 CORSMiddleware 包住整个应用,所有接口统一放行指定来源,是最常用的做法。

python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PATCH", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
    expose_headers=["X-Request-ID"],
    max_age=600,
)

@app.get("/items")
def items():
    return [{"id": 1, "name": "键盘"}]

保存为 main.py,启动 uv run uvicorn main:app --reload。

app.add_middleware(CORSMiddleware, ...) 是 FastAPI 提供的快捷方式,等价于手动包一层 app = CORSMiddleware(app=api, ...)。手动包裹在需要精确控制与异常处理中间件的先后顺序时更直观——把 CORS 放在最外层,才能让应用生成的错误响应也带上跨域头。

模块级跨域 ​

当"不同模块被不同前端来源访问"时,可以给每个模块一个独立的子 FastAPI 应用,各自挂自己的 CORS 配置,再 mount 到主应用。

python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

# 用户模块:独立子应用,自己的 CORS 配置
users_app = FastAPI()
users_app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://user.example.com"],
    allow_methods=["*"],
    allow_headers=["*"],
)

@users_app.get("/users/{user_id}")
def detail(user_id: int):
    return {"id": user_id}

# 商品模块:另一套 CORS 配置
products_app = FastAPI()
products_app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://shop.example.com"],
    allow_methods=["*"],
    allow_headers=["*"],
)

@products_app.get("/products")
def products():
    return [{"id": 1, "name": "键盘"}]

app = FastAPI()
app.mount("/api/users", users_app)
app.mount("/api/products", products_app)

这样 /api/users 只对 http://user.example.com 放行,/api/products 只对 http://shop.example.com 放行。

代价是每个模块变成独立的 ASGI 子应用:路由 URL 会多出 mount 前缀,跨模块共享依赖和统一中间件也更麻烦。绝大多数项目只有一个前端来源,用全局级即可;模块级只在"确实有多个独立前端"时才值得。

路由级跨域 ​

FastAPI 没有原生的路由级 CORS,因为 CORSMiddleware 只能包 ASGI 应用,不能包单个路由。要只对某个接口放行,可以手动在响应头里加 CORS 头:

python
from fastapi import FastAPI, Response

app = FastAPI()

@app.get("/api/open")
def open_api(response: Response):
    response.headers["Access-Control-Allow-Origin"] = "http://localhost:5173"
    return {"message": "仅此接口允许跨域"}

@app.get("/api/private")
def private():
    return {"message": "此接口不允许跨域"}

多个接口需要重复设置时,可以抽一个小工具函数统一加头:

python
def allow_cors(response: Response, origin: str = "http://localhost:5173"):
    response.headers["Access-Control-Allow-Origin"] = origin
    response.headers["Access-Control-Allow-Methods"] = "GET, POST"
    response.headers["Access-Control-Allow-Headers"] = "Content-Type"

要注意:手动加头只对"简单请求"生效。带 JSON、自定义头或非简单方法的请求会先发预检 OPTIONS,手动方案需要额外处理 OPTIONS 才能完整通过。因此路由级跨域通常只适合临时放行或简单 GET;需要完整预检支持时,仍应使用 CORSMiddleware(哪怕是给单个接口包一层子应用)。

若前端需要携带跨域 Cookie,通常还需在 fetch 设置 credentials: "include",并匹配 Cookie 的 SameSite、Secure 和浏览器策略。开启凭据时,应明确列出来源、方法和请求头,不使用随意的全通配配置。

响应头默认也不是全部对脚本可见;自定义编号头要在 expose_headers 里声明。WebSocket 的 Origin 校验需另行处理,不由 HTTP CORS 配置自动完成。

排查顺序 ​

先确认请求是否到达服务器,再看 OPTIONS 的状态与允许头,最后检查真实响应。代理错误、HTTPS 混合内容、服务端 500 也可能在浏览器中表现为跨域提示,不能只盯着中间件配置。

第二部分:静态资源 ​

静态资源挂载基础 ​

FastAPI 用 StaticFiles 把磁盘上的目录挂载到某个 URL 前缀下,文件按原样返回。

python
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()

app.mount("/static", StaticFiles(directory="static"), name="static")

目录结构:

text
project/
├── main.py
└── static/
    ├── style.css
    └── app.js

访问 http://127.0.0.1:8000/static/style.css 返回 static/style.css 的内容。name="static" 是给这个挂载点起的名字,用于后面 url_for 反向生成链接。

挂载多个目录 ​

可以把不同目录挂到不同 URL 前缀下,互不干扰:

python
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/images", StaticFiles(directory="images"), name="images")
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")

适合把 CSS/JS、图片、用户上传分开放,各自有独立的访问路径和后续处理。

用 html=True 服务前端页面 ​

html=True 时,访问目录(如 / 或 /assets/)会自动返回目录下的 index.html,适合挂载打包后的前端产物。

python
app.mount("/", StaticFiles(directory="dist", html=True), name="frontend")

默认 html=False,访问目录会返回 404。注意 html=True 只对"目录"生效;前端单页应用的历史路由(如 /some/route)并不对应真实目录,仍会 404,需要额外的回退处理,这里不展开。

挂载顺序与根路径 ​

路由按注册顺序匹配,挂载到根路径 / 会匹配所有路径,因此要放在 API 路由之后:

python
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()

@app.get("/api/health")
def health():
    return {"ok": True}

# 最后再挂载根路径,避免把 /api/* 也吞掉
app.mount("/", StaticFiles(directory="dist", html=True), name="frontend")

如果把这个根路径挂载放在最前面,/api/health 会先被静态资源匹配,接口就 404 了。

目录不存在时的处理 ​

默认 check_dir=True,目录不存在会直接报错。用户上传目录可能在运行时才创建,用 check_dir=False 允许先挂载、后创建:

python
app.mount("/uploads", StaticFiles(directory="uploads", check_dir=False), name="uploads")

用 url_for 反向生成链接 ​

python
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")

@app.get("/")
def index(request: Request):
    return {"script": request.url_for("static", path="app.js")}

返回 /static/app.js。改挂载路径时,链接会自动跟随,不需要到处硬编码。

安全注意 ​

  • 不要把静态目录指向项目根目录或含 .env、密钥的目录,否则可能泄露敏感文件。
  • 用户上传目录要配合鉴权和文件名处理,避免路径穿越或上传可执行脚本。
  • 静态文件按原样返回,不经过响应模型过滤,不要用 StaticFiles 去服务需要鉴权的私有文件。

练习:把全局 CORS 里的允许来源改成一个未允许的端口,比较预检响应;再新增一个 images 静态目录并挂载到 /images,用 url_for 生成一张图片的访问链接。