FastAPI跨域资源共享

一、为什么会有跨域问题?—— 同源策略详解

在前后端分离项目中,跨域问题是高频痛点,其根源是浏览器的「同源策略(Same-Origin Policy,SOP)」—— 一种浏览器内置的安全机制,用于限制不同源的网页之间的资源交互,防止恶意网站窃取用户敏感数据(如 Cookie、Token、个人信息)。

1.1 同源的判定标准

一个「源」由协议、域名、端口 三个部分共同组成,三者必须完全一致,才被浏览器判定为「同源」;任一部分不同,即为「跨源」。

基准地址(示例) 对比地址 是否同源 原因
https://www.example.com:443 https://www.example.com 默认端口443可省略,协议、域名一致
http://www.example.com 协议不同(https vs http)
https://blog.example.com 域名不同(www vs blog)
https://www.example.com:8080 端口不同(443 vs 8080)

1.2 同源策略的核心限制

同源策略主要限制非同源网页的以下交互:

  • JavaScript 脚本无法读取非同源网页的 Cookie、LocalStorage、SessionStorage;
  • 无法通过 AJAX(fetch、axios 等)请求非同源服务器的资源,浏览器会拦截响应;
  • 无法访问非同源网页的 DOM 元素。

注意:同源策略是「浏览器的限制」,而非服务器的限制。通过 Postman、curl 等工具发起跨域请求,不会被拦截,可正常获取响应。

1.3 跨域需求的必要性

现代 Web 应用几乎都是前后端分离架构(前端部署在 localhost:8080,后端部署在 localhost:8000,或前端部署在域名 A,后端部署在域名 B),必然需要跨源请求。此时,就需要通过 CORS 机制,让服务器授权浏览器放行跨域请求。

二、CORS 核心原理

CORS(Cross-Origin Resource Sharing,跨域资源共享)是基于 HTTP 响应头的解决方案,核心逻辑是:服务器通过返回特定的跨域响应头,告知浏览器「允许哪些源、哪些方法、哪些请求头访问本服务器资源」,浏览器验证通过后,放行跨域请求的响应

2.1 两种跨域请求类型

浏览器会根据请求的类型,将跨域请求分为「简单请求」和「预检请求」,两种请求的处理流程不同,需重点区分。

(1)简单请求(无需预检)

同时满足以下条件的请求,属于简单请求,浏览器会直接发起请求,无需提前询问服务器:

  • 请求方法:仅 GET、POST、HEAD 三种;
  • 请求头:仅包含浏览器默认头,如 Content-Type(仅允许 application/x-www-form-urlencoded、multipart/form-data、text/plain 三种值)、Accept、Accept-Language 等;
  • 无自定义请求头,无请求体(或请求体格式符合上述 Content-Type 要求)。

处理流程:前端发起请求 → 服务器返回响应(携带跨域头) → 浏览器验证跨域头 → 验证通过,展示响应;验证失败,拦截响应。

(2)预检请求(OPTIONS 请求,需提前校验)

不满足简单请求条件的,均为预检请求,浏览器会先发起一次 OPTIONS 请求(预检),询问服务器「是否允许该跨域请求」,服务器允许后,才会发起真正的业务请求。

常见触发预检请求的场景:

  • 请求方法:PUT、DELETE、PATCH 等非简单请求方法;
  • 请求头:包含自定义头(如 Authorization、Token、X-Request-ID);
  • 请求体格式:Content-Type 为 application/json(最常见的场景);
  • 请求中携带 Cookie/认证信息。

处理流程:前端发起 OPTIONS 预检请求 → 服务器返回 200 状态码(携带跨域授权头) → 浏览器验证预检响应 → 验证通过,发起真正的业务请求 → 服务器返回业务响应 → 浏览器展示响应。

2.2 核心跨域响应头

服务器返回的跨域响应头,是浏览器判断是否放行的核心依据,常用头如下:

  • Access-Control-Allow-Origin:最核心的头,指定允许访问的跨域源(如 http://localhost:8080),* 表示允许所有源(仅开发环境可用);
  • Access-Control-Allow-Methods:允许的请求方法(如 GET, POST, PUT, DELETE, OPTIONS);
  • Access-Control-Allow-Headers:允许的请求头(如 Content-Type, Authorization);
  • Access-Control-Allow-Credentials:是否允许携带 Cookie/认证信息(true/false);
  • Access-Control-Max-Age:预检请求的缓存时间(秒),缓存期间,相同的预检请求不会重复发起,提升性能。

三、前端跨域测试代码

创建 cors-test.html 文件,通过 fetch 发起两种类型的跨域请求(简单请求、预检请求),用于验证后端跨域配置是否生效,直接用浏览器打开即可运行。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>FastAPI CORS 跨域测试</title>
  <style>
    button { margin: 10px; padding: 8px 16px; cursor: pointer; }
    #result { margin-top: 20px; font-size: 14px; line-height: 1.5; }
    .success { color: green; }
    .error { color: red; }
  </style>
</head>
<body>
  <h1>FastAPI CORS 跨域测试</h1>
  <button onclick="testSimpleRequest()">1. 测试简单请求(GET)</button>
  <button onclick="testPreflightRequest()">2. 测试预检请求(POST-JSON)</button>
  <div id="result"></div>

  <script>
    // 后端接口地址(FastAPI 运行地址,需与后端一致)
    const baseUrl = 'http://127.0.0.1:8000';
    const resultDom = document.getElementById('result');

    // 测试简单请求(GET,无需预检)
    async function testSimpleRequest() {
      try {
        const response = await fetch(`${baseUrl}/info`, { method: 'GET' });
        const data = await response.text();
        resultDom.innerHTML = `✅ 简单请求成功:<br/>${data}`;
        resultDom.className = 'success';
      } catch (error) {
        resultDom.innerHTML = `❌ 简单请求失败:<br/>${error.message}`;
        resultDom.className = 'error';
      }
    }

    // 测试预检请求(POST + JSON 头,触发 OPTIONS 预检)
    async function testPreflightRequest() {
      try {
        const response = await fetch(`${baseUrl}/submit`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' }, // 触发预检的关键
          body: JSON.stringify({ name: 'FastAPI', type: 'CORS测试' })
        });
        const data = await response.json();
        resultDom.innerHTML = `✅ 预检请求成功:<br/>${JSON.stringify(data, null, 2)}`;
        resultDom.className = 'success';
      } catch (error) {
        resultDom.innerHTML = `❌ 预检请求失败:<br/>${error.message}`;
        resultDom.className = 'error';
      }
    }
  </script>
</body>
</html>

四、FastAPI 跨域解决方案

FastAPI 提供两种跨域解决方案:手动实现 CORS 中间件(适合理解底层原理)、使用官方封装的 CORSMiddleware(推荐生产环境使用),两种方法均可实现跨域支持,按需选择。

方法1:手动实现 CORS 中间件

通过 FastAPI 原生中间件,手动拦截 OPTIONS 预检请求、为所有响应添加跨域头,适合深入理解 CORS 工作流程,代码可直接复制运行。

from fastapi import FastAPI, Request
from fastapi.responses import Response, JSONResponse

# 1. 创建 FastAPI 应用实例
app = FastAPI(title="FastAPI 手动实现 CORS", version="1.0.0")

# 2. 自定义 CORS 中间件(核心逻辑)
@app.middleware("http")
async def custom_cors_middleware(request: Request, call_next):
    """
    手动实现 CORS 中间件
    1. 拦截 OPTIONS 预检请求,直接返回授权响应
    2. 为所有业务请求的响应,添加跨域头
    """
    # 第一步:处理 OPTIONS 预检请求
    if request.method == "OPTIONS":
        # 预检请求无需执行后续路由逻辑,直接返回 200 + 跨域授权头
        cors_headers = {
            "Access-Control-Allow-Origin": "*",  # 开发环境允许所有源
            "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",  # 允许所有方法
            "Access-Control-Allow-Headers": "Content-Type, Authorization",  # 允许常用请求头
            "Access-Control-Max-Age": "86400"  # 预检请求缓存 24 小时,避免重复预检
        }
        return Response(status_code=200, headers=cors_headers)
    
    # 第二步:处理普通业务请求(GET/POST/PUT/DELETE 等)
    # 调用下一个中间件或路由函数,获取业务响应
    response = await call_next(request)
    
    # 为业务响应添加跨域头
    response.headers["Access-Control-Allow-Origin"] = "*"
    response.headers["Access-Control-Allow-Credentials"] = "true"  # 允许携带 Cookie
    
    return response

# 3. 测试接口(用于验证跨域)
# 简单请求接口(GET,无需预检)
@app.get("/info")
async def get_info():
    return "跨域简单请求测试成功!这是 FastAPI 后端返回的内容"

# 预检请求接口(POST + JSON,触发 OPTIONS 预检)
@app.post("/submit")
async def submit_data(data: dict):
    return JSONResponse(
        content={
            "code": 200,
            "msg": "跨域预检请求测试成功",
            "data": data
        }
    )

# 4. 运行服务
if __name__ == "__main__":
    import uvicorn
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
    # 运行后,后端地址:http://127.0.0.1:8000

方法2:使用官方 CORSMiddleware

FastAPI 内置了 fastapi.middleware.cors.CORSMiddleware 中间件,封装了完整的 CORS 逻辑,无需手动处理 OPTIONS 请求,配置简单、功能完善,是生产环境的首选方案。

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

# 1. 创建 FastAPI 应用实例
app = FastAPI(title="FastAPI 官方 CORS 中间件", version="1.0.0")

# 2. 配置并添加官方 CORS 中间件(核心步骤)
app.add_middleware(
    CORSMiddleware,
    # 关键配置参数(按环境按需调整)
    allow_origins=["*"],  # 允许的跨域源(开发环境用 *,生产环境替换为具体前端域名)
    allow_credentials=True,  # 是否允许携带 Cookie/认证信息
    allow_methods=["*"],  # 允许的请求方法(* 表示所有)
    allow_headers=["*"],  # 允许的请求头(* 表示所有)
    max_age=86400,  # 预检请求缓存时间(秒),24小时
)

# 3. 定义数据模型(用于接收 POST-JSON 参数)
class SubmitData(BaseModel):
    name: str
    type: str

# 4. 测试接口(与手动实现一致,用于验证跨域)
# 简单请求接口
@app.get("/info")
async def get_info():
    return "跨域简单请求测试成功!这是 FastAPI 后端返回的内容"

# 预检请求接口
@app.post("/submit")
async def submit_data(data: SubmitData):
    return {
        "code": 200,
        "msg": "跨域预检请求测试成功",
        "data": data.dict()
    }

# 5. 运行服务
if __name__ == "__main__":
    import uvicorn
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)

五、开发/生产环境配置差异

跨域配置的核心风险的是「源的授权范围」,开发环境为了效率可宽松配置,生产环境必须严格限制,避免安全漏洞。

5.1 开发环境配置(宽松,提升效率)

开发环境中,前端通常运行在 localhost:8080/8081,后端运行在 localhost:8000,可使用宽松配置,无需频繁修改:

# 开发环境 CORS 配置(官方中间件)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 允许所有源
    allow_credentials=True,
    allow_methods=["*"],  # 允许所有方法
    allow_headers=["*"],  # 允许所有请求头
    max_age=86400,
)

5.2 生产环境配置(严格,保障安全)

生产环境中,前端部署在具体域名(如 https://web.xxx.com),后端部署在 API 域名(如 https://api.xxx.com),需严格限制源、方法、头,禁止使用 * 通配符。

# 生产环境 CORS 配置(官方中间件,推荐)
app.add_middleware(
    CORSMiddleware,
    # 1. 允许的源:仅添加前端实际部署的域名(多个域名用列表)
    allow_origins=["https://web.xxx.com", "https://m.xxx.com"],
    # 2. 允许携带 Cookie/Token(按需开启)
    allow_credentials=True,
    # 3. 允许的方法:仅开放业务所需的方法(避免不必要的方法暴露)
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    # 4. 允许的请求头:仅开放业务所需的请求头
    allow_headers=["Content-Type", "Authorization"],
    # 5. 预检缓存时间:1小时(3600秒),按需调整
    max_age=3600,
)

生产环境关键注意事项

  • 禁止 allow_origins=["*"]:通配符 * 会允许所有域名访问,存在恶意网站伪造请求的风险,必须替换为具体的前端域名;
  • allow_credentials=True 的限制:当该参数设为 True(允许携带 Cookie/Token)时,allow_origins 不能为 *,必须指定具体域名,否则浏览器会拦截响应;
  • 按需限制方法和请求头:无需开放所有方法和请求头,仅开放业务必需的(如仅允许 GET/POST 方法,仅允许 Content-Type/Authorization 请求头),降低攻击面;
  • 中间件注册顺序:CORSMiddleware 需注册在「所有路由和其他中间件之前」,确保所有请求都能被跨域中间件拦截处理。

六、跨域测试步骤(快速验证)

无论使用哪种方法,均可通过以下步骤快速验证跨域配置是否生效:

  1. 运行 FastAPI 后端代码,确保服务正常启动(地址:http://127.0.0.1:8000);
  2. 用浏览器直接打开 cors-test.html 文件(前端运行在本地文件协议,与后端跨源);
  3. 分别点击两个测试按钮:
    1. 若显示绿色成功提示,说明跨域配置生效;
    2. 若显示红色失败提示,检查后端配置(如跨域头是否添加、中间件是否注册正确)和接口地址是否匹配。
  4. (可选)打开浏览器 F12 开发者工具 → Network 面板,可查看请求详情(如 OPTIONS 预检请求的响应状态、跨域头是否存在)。

七、常见跨域问题排查(高频痛点)

  • 问题1:预检请求(OPTIONS)返回 404/405 原因:未正确处理 OPTIONS 请求,或手动实现中间件时遗漏了 OPTIONS 分支; 解决:使用官方 CORSMiddleware(自动处理 OPTIONS),或在自定义中间件中优先判断 request.method == "OPTIONS" 并返回 200。
  • 问题2:携带 Cookie 时跨域失败,提示「The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*'」 原因:allow_credentials=True 时,allow_origins 设为 *,浏览器禁止该组合; 解决:将 allow_origins 改为具体的前端域名(如 ["https://web.xxx.com"])。
  • 问题3:自定义请求头(如 Authorization)跨域失败 原因:allow_headers 未包含该自定义请求头,或设置为非 * 时遗漏; 解决:在 allow_headers 中添加该请求头(如 ["Content-Type", "Authorization"])。
  • 问题4:前端框架(Vue/React/Angular)跨域失败,Postman 测试正常 原因:前端框架的请求默认携带自定义头(如 axios 默认 Content-Type 为 application/json),触发预检请求,后端未配置对应的跨域头; 解决:使用官方 CORSMiddleware,确保 allow_methods 和 allow_headers 配置正确(开发环境可先用 * 测试)。
  • 问题5:跨域配置后,响应仍被拦截 原因:中间件注册顺序错误(CORSMiddleware 注册在路由之后),部分请求未被拦截; 解决:将 app.add_middleware(CORSMiddleware, ...) 移到所有路由注册之前。
posted @ 2026-02-04 17:30  向闲而过  阅读(1)  评论(0)    收藏  举报