我之前写过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)------框架控制着流程,而你的代码只是"钩"在流程的某个节点上。
为了让你更直观地理解,可以把程序执行想象成一条流水线:
- 普通函数调用:你站在流水线旁边,主动伸手拿起一个零件(数据)去加工。
- 钩子函数:你在流水线的某个位置安装一个感应器,并设定"每当有零件通过这里,就自动启动旁边的机械臂(你的代码)"。你不需要盯着流水线,流水线自己会触发你的机械臂。
钩子函数的三个核心特征:
- 由系统触发:调用者不是你的业务代码,而是框架(如FastAPI)或操作系统。
- 生命周期锚点:它们通常绑定在特定的时间点(如"请求开始前"、"对象创建后"、"程序关闭时")。
- 注入自定义行为:在不修改框架核心源码的前提下,扩展或修改框架的功能。
FastAPI中间件完全符合钩子的定义:
- 锚点:请求进入时、响应返回时。
- 触发者 :FastAPI 框架自动调用你写的
@app.middleware函数。 - 注入行为 :你可以在
call_next之前 和之后挂载自己的逻辑(如加 Header、记日志),这正是利用了"钩"住请求-响应生命周期两端的能力。
钩子不仅仅是 Web 框架的专属,它在软件工程中无处不在:
- 数据库 ORM(如 SQLAlchemy) :提供
before_insert、after_update钩子。当你保存一条记录时,系统自动触发这些函数来更新时间戳或校验数据。 - 前端框架(如 React/Vue) :生命周期钩子(如
onMounted、useEffect)。页面加载完成后,框架自动执行这些函数来获取数据。 - 版本控制工具 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。 | True 或 False |
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
}
验证不成功的场景:
- 访问
GET /my_profile时,请求头没有 Authorization:OAuth2PasswordBearer 会直接返回401 Unauthorized,根本不会执行profile() - 如果 Token 被别人修改,如将
sub = alice改成sub = admin,JWT 的签名立即失效。执行jwt.decode(...)时会抛异常Signature verification failed,返回401 Unauthorized。因此攻击者不能随意修改 JWT 内容。JWT 的内容本身通常是可读的,但依赖签名来保证完整性和真实性,不依赖加密来保密。
上面的例子只是为了说明 JWT 的工作机制。生产环境通常还会增加:
- 使用数据库查询用户,而不是硬编码用户名和密码。
- 使用
bcrypt或argon2存储密码哈希,绝不保存明文密码。 - 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 官方也支持。
其它参考资料
- 参考了ChatGPT和DeepSeek的输出
- Fastapi框架-冷饭再炒-基础知识补充篇(5)- 自定义中间件,在中间获取响应体报文Fastapi 自定义中间件 这 - 掘金