FastAPI后端配置CORS中间件支持浏览器跨域访问

诸神缄默不语-个人技术博文与视频目录

我之前写过FastAPI的介绍博文:深入浅出FastAPI:现代Python Web开发的利器-CSDN博客

本篇博文是专门介绍在开发FastAPI中如何使用CORS中间件机制(CORSMiddleware)来支持浏览器跨域访问。

仅当浏览器前端JS需要与FastAPI后端接口交互时会使用到CORS中间件,所以如果仅作为大模型接口给curl、Python/Java/Go语言脚本、postman等调用时就不需要(CORS中间件也不会起作用,所以不会被阻止访问)。浏览器会在JS请求访问后端服务时检查是否跨域,对于跨域的请求就会根据中间件来判断是否允许访问和返回结果。

FastAPI的CORS中间件默认没有启用 CORSMiddleware,浏览器的跨域请求不会获得允许 CORS 的响应头,因此浏览器通常会阻止跨域 JS 获取响应。所以如果想跨域请求就必须要设置。一般来说网页服务现在都前后端架构分离,那就必须要设置了。

代码写法类似于这样,前端端口是8080,后端运行在 http://localhost (浏览器会将未显式指定端口的 HTTP URL 解释为 80 端口。实际开发中,如果使用 Uvicorn 默认启动(uvicorn python_file_name:app,则通常监听在 8000 端口):

python 复制代码
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = [
    "http://localhost.tiangolo.com",
    "https://localhost.tiangolo.com",
    "http://localhost",
    "http://localhost:8080",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=[
        "GET",
        "POST"
    ],
    allow_headers=["*"],
)


@app.get("/")
async def main():
    return {"message": "Hello World"}

FastAPI CORS官方文档:fastapi.tiangolo.com/zh/tutorial...

1. 什么是中间件?

FastAPI中间件官方文档:fastapi.tiangolo.com/zh/tutorial...

"中间件"是一个函数,加在FastAPI应用上,它会在每个请求之前运行,也会在返回每个响应之前运行。

请求之前运行:

markdown 复制代码
前端发送Request
    │
    ▼
Middleware
    │
    ▼
路由
    │
    ▼
执行操作

返回响应之前运行:

markdown 复制代码
业务处理
    │
    ▼
路由返回Response
    │
    ▼
Middleware
    │
    ▼
前端

除了CORS之外,中间件还可以:

  • 修改请求
  • 拒绝请求
  • 转发请求
  • 添加 Header
  • 打日志
  • 统计时间
  • 身份认证
  • 限流
  • 压缩响应
  • 后处理

等等。

CORS只是Middleware的一个用途。

FastAPI(准确说是底层的 Starlette)还提供了别的中间件(Middleware),例如:

  • GZipMiddleware(压缩)
  • HTTPSRedirectMiddleware(强制将HTTP请求重定向到HTTPS)
  • TrustedHostMiddleware(限制 Host)等。

FastAPI添加内置中间件可以直接使用 app.add_middleware() 方法,如果要自定义中间件可以用@app.middleware("http") 装饰器,如一个计算请求被处理的用时并将其添加到headers中的中间件示例,其中call_next()的执行,就会将请求继续传递给后续ASGI应用(包括后续中间件和最终路由),如果没有后续中间件,就开始执行实际路由到的后端处理函数:

python 复制代码
import time
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    # 1. 记录请求开始时间(预处理)
    start_time = time.perf_counter()

    # 2. 将请求传递给路径操作函数,并等待响应
    response = await call_next(request)

    # 3. 计算处理时间,并添加到响应头中(后处理)
    process_time = time.perf_counter() - start_time
    response.headers["X-Process-Time"] = str(process_time)

    # 4. 返回响应
    return response

相关概念:钩子(hook)函数

钩子(Hook)函数,本质上是"框架留出来的一个插口",让你能够将自定义代码"挂载"到系统已有的执行流程上。

换句话说,它不是你主动去调用的函数,而是由系统在特定事件发生时自动去调用的函数 。这体现了控制反转(IoC)------框架控制着流程,而你的代码只是"钩"在流程的某个节点上。

为了让你更直观地理解,可以把程序执行想象成一条流水线

  • 普通函数调用:你站在流水线旁边,主动伸手拿起一个零件(数据)去加工。
  • 钩子函数:你在流水线的某个位置安装一个感应器,并设定"每当有零件通过这里,就自动启动旁边的机械臂(你的代码)"。你不需要盯着流水线,流水线自己会触发你的机械臂。

钩子函数的三个核心特征:

  1. 由系统触发:调用者不是你的业务代码,而是框架(如FastAPI)或操作系统。
  2. 生命周期锚点:它们通常绑定在特定的时间点(如"请求开始前"、"对象创建后"、"程序关闭时")。
  3. 注入自定义行为:在不修改框架核心源码的前提下,扩展或修改框架的功能。

FastAPI中间件完全符合钩子的定义:

  • 锚点:请求进入时、响应返回时。
  • 触发者 :FastAPI 框架自动调用你写的 @app.middleware 函数。
  • 注入行为 :你可以在 call_next 之前之后挂载自己的逻辑(如加 Header、记日志),这正是利用了"钩"住请求-响应生命周期两端的能力。

钩子不仅仅是 Web 框架的专属,它在软件工程中无处不在:

  • 数据库 ORM(如 SQLAlchemy) :提供 before_insertafter_update 钩子。当你保存一条记录时,系统自动触发这些函数来更新时间戳或校验数据。
  • 前端框架(如 React/Vue) :生命周期钩子(如 onMounteduseEffect)。页面加载完成后,框架自动执行这些函数来获取数据。
  • 版本控制工具 Git :Git 的 pre-commit 钩子。在你执行 git commit 之前,系统自动运行代码格式检查,如果不通过就阻止提交。

总结一句话 :钩子函数就是 "框架叫你一声,你敢答应吗?" ------只要你用特定的装饰器(@app.middleware)或命名规则写好了函数,框架在关键时刻就会来调用你,让你介入流程。

2. 什么是CORS(Cross-Origin Resource Sharing, 跨域资源共享)?

MDN文档:developer.mozilla.org/zh-CN/docs/...

CORS(跨域资源共享)是浏览器实现的一套安全机制。指浏览器前端 JavaScript 与后端接口通信时,后端处于与前端不同的「源」的情况。

源是协议 (http,https)、域名myapp.com,localhost,localhost.tiangolo.com)以及端口(80、443、8080)的组合。

因此,这些都是不同的源:

即使它们都在 localhost 中,但是它们使用不同的协议或者端口,所以它们都是不同的「源」。

(可以用*通配符表示所有源,即允许所有源的前端JS代码访问)

如果没有任何防护机制的话,在浏览器中,假设你登录了银行网站(bank.com)之后,session就存在于浏览器中,此时再登录恶意网站(evil.com),恶意网站的JS要求调用银行网站的接口:

javascript 复制代码
fetch("https://bank.com/api/transfer")

如果浏览器什么都不管,就会服从恶意网站的要求,用你在银行网站的session去请求银行网站的接口了。

(这里现代cookie会设置SameSite=Lax。不过这不是本期重点,我就不展开了。我们先假设浏览器会保存session)

所以浏览器为了防止恶意网页偷偷访问别的网站,默认实行Same Origin Policy(SOP, 同源策略 )。

除非银行网站在CORS中显式允许被evil.com访问,也就是在header中加上Access-Control-Allow-Origin。(还可以设置允许什么方法、什么认证机制等)

CORS中间件就是用于增加这些HTTP header的:

Access-Control-Allow-Origin

Access-Control-Allow-Methods

Access-Control-Allow-Headers

Access-Control-Allow-Credentials

访问执行流程是这样:

scss 复制代码
浏览器打开网页
        │
        ▼
JavaScript 执行 fetch()
        │
        ▼
浏览器发现origin不同
        │
        ▼
决定是直接请求,还是先OPTIONS(如果OPTIONS后发现服务器禁止请求,将停止流程)
        │
        ▼
浏览器发送 HTTP 请求
        │
        ▼
FastAPI 收到请求
        │
        ▼
FastAPI 返回 Response
        │
        ▼
浏览器检查 CORS 响应头
        │
   ┌────┴────┐
   │         │
允许       不允许
   │         │
   ▼         ▼
JS得到数据   浏览器拦截

所以一般来说,对于现代前后端架构分离的web应用,后端就需要考虑CORS设置,这样前端JS代码才能正常访问后端应用。

但是,如果前端不是浏览器,而是客户端直接调用API接口(比如Python脚本直接调用大模型API服务),那就不需要考虑CORS了。因此,CORS中间件不能作为 API 的身份认证或访问控制机制,它仅限制浏览器中的跨域访问行为。

CORS还可以设置是否允许:

  • 浏览器凭证(如 Cookie、HTTP 认证信息等),实际开发中通常也需要考虑 Authorization 请求头。
  • 特定的 HTTP 方法(POST,PUT)或者使用通配符 * 允许所有方法。
  • 特定的 HTTP 请求头或者使用通配符 * 允许所有请求头。

Preflight预检请求

对于简单请求,浏览器会直接向服务器发送请求,在后端返回结果后再检查前端是否会得到数据;对于Preflight预检请求,会在调用服务之前就先发送OPTIONS请求(是安全的方法),查询服务器是否支持指定的域、方法和认证机制,通过后(如Access-Control-Allow-Origin: http://localhost:8080)才发送请求。

这是为了防止有些请求本身就会影响后端数据。

简单请求(Simple Request)必须满足:

  • GET/HEAD/POST
  • Content-Type为如下三种之一:
    application/x-www-form-urlencoded
    multipart/form-data
    text/plain
    所以,现代 React、Vue、Angular 项目几乎都会触发 Preflight,因为 JSON 请求(Content-Type: application/json)已经成为主流。
  • 无自定义Header

具体的请求头和响应头

CORS机制完全由以下请求头和响应头的配对验证实现:

请求头(浏览器自动生成)

  • Origin:携带当前发起请求的源。所有跨域请求都带此头。
  • Access-Control-Request-Method:预检请求中携带,告知服务器实际请求将使用的方法。
  • Access-Control-Request-Headers:预检请求中携带,告知服务器实际请求将携带的自定义头部。

响应头(服务器必须精准返回)

  • Access-Control-Allow-Origin核心字段 。服务器指定允许访问的源。若值为 *,表示允许所有源;若为具体源(如 https://a.com),必须与请求头的 Origin 精确匹配。
  • Access-Control-Allow-Methods:预检响应中返回,逗号分隔允许的HTTP方法列表。
  • Access-Control-Allow-Headers:预检响应中返回,逗号分隔允许的请求头列表。
  • Access-Control-Allow-Credentials:布尔值。若为 true,表示允许浏览器携带Cookie和认证凭证。注意 :当该字段为 true 时,Allow-Origin 严禁 使用通配符 *,必须指定明确的源。
  • Access-Control-Expose-Headers:默认情况下,JS只能访问基本响应头(如Cache-Control等)。此字段指定服务器允许JS额外访问的自定义响应头。
  • Access-Control-Max-Age:指定本次预检响应结果的缓存时间(秒)。在有效期内,浏览器不再发送重复的预检请求,直接使用缓存策略。
  • Vary: Origin:当服务器针对不同来源(Origin)返回不同的 Access-Control-Allow-Origin 时,会添加该响应头,告诉浏览器和缓存代理在缓存响应时需要区分不同的 Origin,避免将针对一个来源生成的响应错误地复用于另一个来源。

3. FastAPI CORSMiddleware参数格式

设置逻辑与上一节(具体的请求头和响应头)基本一致:

参数 说明 示例
allow_origins 允许跨域请求的源列表。这是最关键的配置。 ['https://example.org', 'https://www.example.org']['*'](允许所有,但不够安全)
allow_origin_regex 使用正则表达式来匹配允许的源,更灵活。 'https://.*\.example\.org'
allow_methods 允许的HTTP方法 ,如 GET, POST, PUT 等。默认为 'GET'。不用添加OPTIONS,会处理的 ['GET', 'POST']['*'](允许所有)
allow_headers 允许的HTTP请求头 。默认为 []。Accept、Accept-Language、Content-Language 以及 Content-Type 这几个请求头在简单 CORS 请求中总是被允许。 ['X-Requested-With', 'Content-Type']['*'](允许所有)
allow_credentials 是否允许前端请求携带凭证(如 Cookies)。默认是 False。 TrueFalse
expose_headers 允许前端JavaScript访问的响应头 。默认为 [] ['X-Process-Time']
max_age 浏览器缓存预检请求结果的时间(秒),减少预检请求次数。默认为 600。 600

当 allow_credentials 设为 True 时,allow_origins不能设为 ['*']。必须显式指定。

4. 在浏览器场景之外,如何限制访问API的源、方法等?

1. JWT认证

对JWT的介绍请参考我之前撰写的博文:一篇看懂JWT:Web安全的"身份证"

下面是一个FastAPI + JWT的简单例子,真实项目一般都会在此基础上增加数据库、密码加密、Refresh Token、权限控制等功能。

假设你开发一个企业内部知识库系统。

API 有三个接口:

bash 复制代码
POST /login          登录
GET  /books          所有人都能看
GET  /my_profile     必须登录才能看

访问流程如下:

makefile 复制代码
① 用户登录
        │
        ▼
POST /login
username=alice
password=123456
        │
        ▼
FastAPI验证账号密码
        │
        ▼
返回JWT
{
    "access_token": "...",
    "token_type": "bearer"
}
        │
        ▼
以后访问接口时

Authorization:
Bearer eyJhbGciOi...
        │
        ▼
FastAPI验证JWT
        │
        ▼
JWT合法
        │
        ▼
允许访问

注意:

服务器以后不用保存登录状态(Session)

JWT 本身就证明了:

我已经登录过了,并且 Token 没过期。

FastAPI后端代码实例:

bash 复制代码
pip install fastapi uvicorn pyjwt
python 复制代码
from datetime import datetime, timedelta, timezone

import jwt
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel

SECRET_KEY = "my-secret-key"  # 生产环境请使用随机生成的256bit以上密钥。
ALGORITHM = "HS256"

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")  # 这个斜杠不加也行,因为FastAPI 能解析相对路径


# -------- 登录请求 --------

class LoginRequest(BaseModel):
    username: str
    password: str


@app.post("/login")
def login(data: LoginRequest):

    # 实际项目这里查数据库
    if data.username != "alice" or data.password != "123456":
        raise HTTPException(401, "用户名或密码错误")

    payload = {
        "sub": data.username,
        "exp": datetime.now(timezone.utc) + timedelta(hours=1)
    }

    token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

    return {
        "access_token": token,
        "token_type": "bearer"
    }


# -------- JWT验证 --------

def get_current_user(token: str = Depends(oauth2_scheme)):

    try:
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=[ALGORITHM]
        )

        return payload["sub"]

    except Exception:
        raise HTTPException(401, "Token无效")
        # jwt.ExpiredSignatureError是过期,jwt.InvalidTokenError是篡改


# -------- 公开接口 --------

@app.get("/books")
def books():
    return ["Python", "FastAPI", "LLM"]


# -------- 需要登录 --------

@app.get("/my_profile")
def profile(username=Depends(get_current_user)):
    return {
        "username": username,
        "vip": True
    }

验证不成功的场景:

  1. 访问GET /my_profile时,请求头没有 Authorization:OAuth2PasswordBearer 会直接返回401 Unauthorized,根本不会执行 profile()
  2. 如果 Token 被别人修改,如将sub = alice改成sub = admin,JWT 的签名立即失效。执行jwt.decode(...)时会抛异常Signature verification failed,返回401 Unauthorized。因此攻击者不能随意修改 JWT 内容。JWT 的内容本身通常是可读的,但依赖签名来保证完整性和真实性,不依赖加密来保密。

上面的例子只是为了说明 JWT 的工作机制。生产环境通常还会增加:

  • 使用数据库查询用户,而不是硬编码用户名和密码。
  • 使用 bcryptargon2 存储密码哈希,绝不保存明文密码。
  • Access Token(例如 15~30 分钟)+ Refresh Token(例如 7 天)的双 Token 机制。
  • 在 JWT 中加入角色(Role)、权限(Scope)等声明,实现 RBAC 权限控制。
  • 全程使用 HTTPS,防止 Token 在传输过程中被窃取。
  • 提供注销、Refresh Token 轮换、Token 吊销等机制。FastAPI 官方的安全教程(fastapi.tiangolo.com/tutorial/se...)也是以这种 OAuth2 Password Flow + JWT 的模式作为推荐实现。

2. API Key

没有key返回401或403

3. IP 白名单

可以在FastAPI中增加中间件实现这样的功能:

python 复制代码
client = request.client.host

if client not in whitelist:
    return 403

更常见的是在Nginx、Apache、云负载均衡、防火墙配置 IP 白名单。这种方式比应用层更可靠。

需要注意的是,如果 FastAPI 部署在 Nginx、Traefik 等反向代理之后,需要正确配置 X-Forwarded-For 或 ProxyHeadersMiddleware,否则 request.client.host 获取到的可能只是代理服务器 IP。

4. 反向代理

markdown 复制代码
Internet
     │
     ▼
 Nginx
     │
     ▼
 FastAPI

Nginx 可以:

  • IP 白名单
  • HTTPS
  • 限流
  • 黑名单
  • Basic Auth
  • WAF

企业生产环境几乎都会这么部署。

5. OAuth2 / OpenID Connect

适用于:

  • 企业平台
  • 第三方登录
  • 多用户系统

FastAPI 官方也支持。

其它参考资料

  1. 参考了ChatGPT和DeepSeek的输出
  2. Fastapi框架-冷饭再炒-基础知识补充篇(5)- 自定义中间件,在中间获取响应体报文Fastapi 自定义中间件 这 - 掘金
相关推荐
Fanta丶1 小时前
14.Activiti7 网关 排他网关ExclusiveGateway、并行网关ParallelGateway
后端
明月_清风1 小时前
robots.txt 完全指南:从入门到精通
后端·爬虫
jvmind_dev1 小时前
Java GC 实战指南(番外篇):被忽视的隐形杀手 —— Class Unloading 如何拖垮 GC
java·后端
明月_清风1 小时前
从零写一个"像人"的爬虫:反爬攻防实战指南
后端·爬虫
贰先生1 小时前
Xiuno BBS 审计之问题13:管理员发帖 doctype=0 时存储型 XSS
后端
贰先生2 小时前
Xiuno BBS 审计之问题12:HTTPS 请求关闭 SSL 证书校验,存在中间人攻击风险
后端
HZ_YZ2 小时前
清理服务器硬盘脚本
后端
布朗克1682 小时前
Go 入门到精通-16-字符串深入
开发语言·后端·golang·字符串
北冥you鱼2 小时前
Go flag 包详解:从命令行解析到实战应用
开发语言·后端·golang