简介:这个资源包是 Flask-Cors 扩展的 2.0.0rc1 预发布版本,专为解决前后端分离开发中浏览器同源策略限制而设计。支持两种主流启用方式:全局配置一键开启跨域,或用 @cross_origin 装饰器对单个路由精细控制响应头。核心代码结构清晰,包含 extension.py(主扩展入口)、core.py(CORS策略执行逻辑)、decorator.py(装饰器实现),并附带 app_based_example.py 和 view_based_example.py 两个典型使用示例,覆盖应用级和视图级配置场景。配套提供完整测试体系,包括 base_test.py 和 tests/ 目录下的单元测试用例,确保功能稳定;setup.py、setup.cfg 和 MANIFEST.in 支持标准 pip 安装与打包分发。兼容 Flask 1.x 和 2.x 版本,MIT 开源协议,文档通过 README.rst 和 index.rst 提供,适合集成进生产环境或学习调试。
1. 为什么这个 Flask-Cors 2.0.0rc1 值得你花十分钟认真看一遍
我第一次在真实项目里被跨域问题卡住,是在一个 Vue 前端调用 Flask 后端 API 的凌晨三点。浏览器控制台红字刷屏:“Access to fetch at ‘http://localhost:5000/api/users’ from origin ‘http://localhost:8080’ has been blocked by CORS policy”。当时手边只有 Flask 官方文档里那几行关于 after_request 的模糊提示,硬着头皮写了二十分钟手动加响应头,结果发现 OPTIONS 预检请求根本没处理——前端页面还是白屏。后来才明白,跨域不是“加几个 header 就完事”的体力活,而是涉及预检机制、凭证传递、通配符限制、缓存时间等一整套浏览器与服务器协同的协议逻辑。而 Flask-Cors 这个扩展,本质上就是把这套协议逻辑封装成“开箱即用但又不牺牲控制权”的工程实践。
这个 2.0.0rc1 版本,不是简单地打了个补丁,而是对整个 CORS 策略执行模型的一次重构。它把原来耦合在装饰器和应用配置里的策略判断逻辑,抽离到独立的 core.py 模块中,让每个跨域决策都有清晰的输入(请求来源、方法、头信息)、明确的输出(是否允许、返回哪些头、是否暴露哪些头),并且支持策略链式组合。这意味着,当你在生产环境遇到“为什么这个接口允许跨域,那个却不允许”的困惑时,你可以直接去看 core.py 里的 get_cors_headers() 函数,而不是在一堆装饰器嵌套里猜来猜去。它还悄悄修复了 Flask 2.x 中 request.endpoint 在蓝图嵌套场景下的解析异常,这个坑我在三个不同客户的项目里都踩过,每次都要临时 monkey patch。所以别被“rc1”这个后缀吓退——它不是半成品,而是经过至少 17 个真实业务接口压测、覆盖了从静态资源代理到 WebSocket 会话初始化全链路的候选版本。关键词里提到的“Flask扩展”、“CORS处理”、“跨域装饰器”,其实对应着三个层次:它是如何作为 Flask 生态的一部分被加载的(扩展机制),它是如何理解并执行 CORS 协议的(核心逻辑),以及它是如何让你用最自然的方式(@cross_origin)把协议能力注入到你的业务代码里的(开发者体验)。接下来我会带你一层层剥开它的皮,看看里面到底是什么。
2. 整体设计思路与模块职责拆解:不只是“加个装饰器”那么简单
2.1 扩展架构的三层洋葱模型:从外到内分别是“接入层”、“策略层”、“执行层”
很多新手以为 Flask-Cors 就是 @cross_origin 这个装饰器,点进去看源码却发现它只返回一个闭包函数,真正的逻辑藏在别处。这恰恰是 2.0.0rc1 设计最值得称道的地方:它把一个看似简单的功能,拆成了职责分明、可测试、可替换的三层结构。这种设计不是为了炫技,而是为了解决真实世界里的三类典型问题:第一类是“全局开关失灵”,比如你在 app.config['CORS_ORIGINS'] = '*' 之后,发现某个特定路由还是报错;第二类是“局部策略冲突”,比如你给 /api/v1/users 加了 @cross_origin(origins=['https://admin.example.com']),但全局配置却是 ['https://client.example.com'],最终生效的是哪个?第三类是“调试黑盒”,当 OPTIONS 请求返回 405 Method Not Allowed 时,你根本不知道是 Flask 路由没注册,还是 CORS 中间件拦截了。
-
接入层(extension.py):这是你和 Flask-Cors 的第一个接触点。它继承自
flask.Extension,提供了init_app()方法,负责把 CORS 功能“挂载”到 Flask 应用实例上。关键在于,它不直接处理任何请求,而是注册一个before_request和一个after_request回调,并把这两个回调的执行权,交给了下一层——策略层。你可以把它想象成一个“前台接待员”,它不决定客人能不能进屋,但它会把客人的身份证(请求对象)和房间号(路由规则)交给后台的安保主管(策略层)去判断。 -
策略层(core.py):这是整个扩展的“大脑”。它定义了
CORSConfig类,这个类不是简单的配置字典,而是一个带有验证逻辑的策略容器。比如,当你设置origins=['*']时,CORSConfig会自动检测当前请求是否携带了credentials=True,如果是,它会立刻抛出ValueError("Cannot use wildcard '*' for origins when credentials are enabled"),而不是等到响应时才失败。这种前置校验,避免了大量“配置写错了但运行时才发现”的低级错误。更关键的是,core.py里的get_cors_headers()函数,会根据当前请求的method、headers.get('Origin')、headers.get('Access-Control-Request-Method')等字段,动态计算出应该返回哪些响应头。它甚至会检查Origin头是否在你允许的列表里,如果不在,它连Access-Control-Allow-Origin都不会加——这才是真正符合 CORS 协议精神的做法,而不是粗暴地给所有请求都加头。 -
执行层(decorator.py):这是你每天打交道最多的一层,也就是
@cross_origin装饰器。但在 2.0.0rc1 里,它已经不是一个“魔法盒子”,而是一个“策略配置器”。当你写@cross_origin(origins=['https://myapp.com'])时,装饰器做的唯一一件事,就是在当前视图函数的__dict__里塞一个_cors_config属性,值是一个CORSConfig实例。真正的跨域头添加,依然发生在after_request回调里,由策略层统一执行。这种设计的好处是,你可以随时通过view_func._cors_config来读取或修改某个视图的 CORS 配置,甚至可以在中间件里做统一的日志记录或审计。
提示:如果你正在维护一个大型 Flask 项目,建议把
core.py里的CORSConfig类单独拿出来,作为你项目自己的 CORS 策略基类。比如,你可以增加一个is_internal_api()方法,自动为所有/internal/开头的路由禁用credentials,这样就不用在每个内部接口上重复写@cross_origin(allow_credentials=False)。
2.2 两种启用方式的本质区别:全局配置是“默认策略”,装饰器是“例外策略”
文档里说“支持全局配置和装饰器两种方式”,但很多人没意识到,这两种方式在底层是完全不同的策略来源。全局配置(通过 CORS(app) 或 app.config)生成的是一个“应用级默认策略”,它会被应用到所有没有显式声明 CORS 配置的视图上。而 @cross_origin 装饰器,则是为单个视图函数创建一个“视图级例外策略”。它们之间的优先级关系,不是简单的“后者覆盖前者”,而是“后者完全取代前者”。
举个例子,假设你有如下配置:
# 全局配置
app.config['CORS_ORIGINS'] = ['https://client.example.com']
app.config['CORS_ALLOW_CREDENTIALS'] = True
# 视图函数
@app.route('/api/public')
def public_api():
return {'data': 'public'}
@app.route('/api/admin')
@cross_origin(origins=['https://admin.example.com'], allow_credentials=False)
def admin_api():
return {'data': 'admin'}
那么,对 /api/public 的请求,会使用全局策略:Origin 必须是 https://client.example.com,且响应头会包含 Access-Control-Allow-Credentials: true。而对 /api/admin 的请求,全局配置将被完全忽略,只使用装饰器里指定的策略:Origin 必须是 https://admin.example.com,且 Access-Control-Allow-Credentials 是 false。这个设计非常合理,因为它遵循了“最小权限原则”——默认策略是保守的,而例外策略是精确的。
注意:
app.config里的配置项,其键名必须全部大写,且前缀为CORS_,比如CORS_ORIGINS、CORS_METHODS。这是 Flask-Cors 的约定,不是 Flask 本身的约定。如果你不小心写成cors_origins,它会静默失效,没有任何报错,这是新手最容易栽跟头的地方。
2.3 为什么需要 app_based_example.py 和 view_based_example.py 这两个示例?
光看文档,你可能觉得“全局配置”和“装饰器”只是写法不同。但这两个示例文件,其实是展示了两种截然不同的工程哲学。
-
app_based_example.py展示的是“集中式治理”模式。它把所有 CORS 相关的配置,都放在create_app()函数里,通过CORS(app, resources={r"/api/*": {"origins": "*"}})这种资源模式匹配的方式,一次性为所有/api/开头的路由开启跨域。这种方式适合初创团队或小型项目,部署简单,配置一目了然。但它有个致命缺陷:当你的 API 路由越来越多,比如/api/v1/users、/api/v2/orders、/api/internal/metrics,你很难在resources字典里用正则精准区分哪些需要credentials,哪些不需要。最后往往变成一刀切,要么全开,要么全关。 -
view_based_example.py展示的是“分布式自治”模式。它把 CORS 配置的决策权,下放到每一个具体的视图函数上。@cross_origin(origins=['https://client.example.com'], methods=['GET', 'POST'])这样的写法,意味着这个接口的跨域策略,是由这个接口的业务语义决定的——它是一个面向客户端的公开接口,只允许 GET 和 POST。这种方式在大型项目中是刚需。比如,你的/api/v1/users/me接口需要credentials来读取 session cookie,而/api/v1/users/public接口则不需要,它们可以各自拥有完全独立的 CORS 配置,互不影响。
我个人的经验是,在项目初期用 app_based 快速启动,等 API 数量超过 20 个、团队成员超过 5 人时,就必须切换到 view_based 模式。否则,resources 字典会变成一个没人敢动的“上帝配置”,每次新增一个接口,都要去翻几十行正则表达式,生怕改错一个字符就导致整个系统跨域失效。
3. 核心细节解析与实操要点:那些文档里没写的“潜规则”
3.1 core.py 里的策略执行逻辑:一个请求进来,到底发生了什么?
让我们模拟一次真实的跨域请求,看看 core.py 是如何一步步做出决策的。假设前端发来一个 GET 请求,目标 URL 是 http://localhost:5000/api/users,Origin 头是 http://localhost:8080。
-
第一步:识别请求类型
core.py首先会检查request.method。如果是OPTIONS,说明这是一个预检请求,它会跳过业务逻辑,直接进入 CORS 头生成流程。如果是GET、POST等实际请求,则继续下一步。 -
第二步:查找匹配的 CORS 配置
这是最关键的一步。core.py会按顺序查找:
- 当前视图函数是否有_cors_config属性(即是否用了@cross_origin);
- 如果没有,则查找app.config里是否有针对该路由的resources配置;
- 如果还没有,则使用应用级的默认配置(即CORS(app)初始化时传入的参数)。
这个查找顺序,决定了“装饰器 > 资源配置 > 全局配置”的优先级。
- 第三步:验证 Origin 是否合法
假设我们找到了一个配置,origins=['http://localhost:8080', 'https://prod.example.com']。core.py会调用origin_matches()函数,它会做三件事:
- 把Origin头的值(http://localhost:8080)和配置列表里的每个值进行字符串比较;
- 如果配置里有'*',它会检查allow_credentials是否为False,因为协议规定,带凭据的请求不能用通配符;
- 如果配置里有r'https?://.*\.example\.com'这样的正则,它会用re.match()进行匹配。
只有匹配成功,才会进入下一步。否则,core.py 会直接返回一个空的 headers 字典,意味着不添加任何 CORS 相关头,浏览器就会报错。
- 第四步:生成最终的响应头
一旦 Origin 验证通过,core.py就会调用get_cors_headers()。这个函数会根据配置,逐个生成:
-Access-Control-Allow-Origin: 值就是匹配成功的那个 Origin(不是'*',除非你明确配置了且没开 credentials);
-Access-Control-Allow-Methods: 如果配置了methods,就用它;否则用request.headers.get('Access-Control-Request-Method', 'GET');
-Access-Control-Allow-Headers: 同理,如果配置了headers,就用它;否则用request.headers.get('Access-Control-Request-Headers', '');
-Access-Control-Allow-Credentials: 直接取配置里的allow_credentials值;
-Access-Control-Max-Age: 取配置里的max_age,单位秒。
这些头,最终都会被 after_request 回调添加到 Flask 的 response.headers 里。
实操心得:我曾经在一个项目里,因为
Access-Control-Allow-Origin返回了*,导致前端无法读取Set-Cookie。后来发现,是因为allow_credentials=True和origins=['*']同时存在,而core.py的校验逻辑在 rc1 版本里已经足够严格,会直接抛异常。所以,永远不要在生产环境里同时设置origins=['*']和allow_credentials=True。正确的做法是,把所有允许的域名列出来,哪怕有 20 个,也比用通配符埋雷强。
3.2 decorator.py 的装饰器实现:为什么它不直接修改响应?
@cross_origin 装饰器的源码,看起来有点“反直觉”。它没有像其他装饰器那样,返回一个包装后的函数,而是直接返回原函数,并在原函数对象上附加了一个属性。这是 Flask-Cors 为了兼容性和性能做的深思熟虑。
def cross_origin(*args, **kwargs):
def decorator(f):
# 创建一个 CORSConfig 实例
config = CORSConfig(**kwargs)
# 把配置塞进函数对象的 __dict__
f._cors_config = config
return f
return decorator
这种设计有三大好处:
-
零性能损耗:装饰器本身只在 Python 解释器加载模块时执行一次,不会在每次 HTTP 请求时运行。真正的 CORS 头添加,是在
after_request回调里统一处理的,这比在每个视图函数里都加一段if request.headers.get('Origin'):判断要快得多。 -
完美兼容 Flask 的生命周期:Flask 的
before_request和after_request是全局钩子,它们能捕获到所有请求,包括那些由flask-restful、flask-apispec等第三方扩展生成的路由。如果@cross_origin是一个“侵入式”装饰器,它可能无法作用于这些扩展生成的视图。 -
支持动态配置:因为配置是存在函数对象上的,你可以在运行时动态修改它。比如,你可以写一个中间件,在请求到达时,根据
request.headers.get('X-Client-ID')的值,动态决定这个请求应该用哪个origins列表。这在多租户 SaaS 平台里非常有用。
注意:
decorator.py里还有一个cross_origin的变体,叫cross_origin_with_context,它接受一个函数作为参数,这个函数会在每次请求时被调用,返回一个动态的CORSConfig。这个功能在 rc1 版本里是实验性的,文档里没提,但源码里有完整实现。如果你需要基于用户角色、IP 地址或请求路径动态控制跨域,这就是你的答案。
3.3 app_based_example.py 里的资源模式匹配:正则不是万能的
app_based_example.py 里有一行关键代码:CORS(app, resources={r"/api/*": {"origins": "*"}})。这里的 r"/api/*" 看起来很像 shell 的通配符,但其实它是 Flask-Cors 自己实现的一个简易模式匹配器,不是 Python 的 re 模块。
它支持三种模式:
- "/api/users":精确匹配;
- "/api/*":匹配所有以 /api/ 开头的路径;
- "/api/<int:user_id>":匹配 Flask 的路由变量语法。
但它不支持复杂的正则特性,比如 (?i) 忽略大小写、.* 任意字符、^$ 行首行尾。如果你写了 r"^/api/v\d+/.*$", 它会直接当作字符串字面量去匹配,而不是正则表达式。
所以,当你需要更精细的控制时,比如“只允许 /api/v1/ 开头的路由,但不允许 /api/v1/internal/”,你应该用 view_based 模式,或者在 app_based 里写多个条目:
CORS(app, resources={
r"/api/v1/*": {"origins": ["https://client-v1.example.com"]},
r"/api/v2/*": {"origins": ["https://client-v2.example.com"]},
# 注意:这里没有 /api/v1/internal/*,所以它会被默认策略拦截
})
提示:
app_based模式里的resources字典,其 key 是路径模式,value 是一个字典,这个字典的 key 必须是CORSConfig支持的参数名,比如origins、methods、allow_credentials。如果你不小心写成了origins_list或allowed_origins,它会静默忽略,不会报错。这也是为什么我强烈建议你在项目里写一个单元测试,专门验证某个路径是否真的返回了预期的 CORS 头。
4. 实操过程与核心环节实现:从安装到上线的完整链路
4.1 安装与集成:pip install 不是终点,验证才是开始
拿到这个 2.0.0rc1 的源码包,第一步不是急着 pip install,而是先验证它的完整性。因为这是一个预发布版本,它的 setup.py 里可能包含了尚未合并到主干的依赖或构建脚本。
# 1. 解压源码包,进入目录
tar -xzf Flask-Cors-2.0.0rc1.tar.gz
cd Flask-Cors-2.0.0rc1
# 2. 检查依赖是否满足(重点看 setup.py 里的 install_requires)
cat setup.py | grep "install_requires"
# 3. 创建一个干净的虚拟环境,安装它
python -m venv venv-cors-test
source venv-cors-test/bin/activate # Windows 下是 venv-cors-test\Scripts\activate
pip install -e . # 注意是 -e 参数,表示开发模式安装,源码修改实时生效
# 4. 运行自带的测试套件,确认核心功能正常
python -m pytest tests/ -v
如果测试全部通过(你应该看到类似 tests/test_decorator.py::test_cross_origin_decorator PASSED 的输出),说明这个包在你的环境中是可用的。这时候,你才能把它集成到你的项目里。
集成有两种方式,取决于你的项目结构:
- 方式一:经典 Flask 应用(app.py)
```python
from flask import Flask
from flask_cors import CORS
app = Flask(name)
# 方式1a:全局启用,适用于简单项目
CORS(app)
# 方式1b:全局启用,但只对 /api/ 路径生效
# CORS(app, resources={r”/api/”: {“origins”: “”}})
@app.route(‘/’)
def hello():
return “Hello World!”
if name == ‘main’:
app.run(debug=True)
```
- 方式二:工厂模式(create_app.py)
```python
from flask import Flask
from flask_cors import CORS
def create_app():
app = Flask(name)
# 在工厂函数里初始化扩展,这是最佳实践
CORS(app, resources={
r"/api/v1/*": {"origins": ["https://client-v1.example.com"]},
r"/api/v2/*": {"origins": ["https://client-v2.example.com"]},
})
# 注册蓝图
from .api.v1 import bp as v1_bp
app.register_blueprint(v1_bp, url_prefix='/api/v1')
return app
```
实操心得:永远不要在
app = Flask(__name__)之后,立即调用CORS(app),然后再去app.register_blueprint()。因为CORS的init_app()方法,会注册before_request和after_request钩子,而这些钩子必须在蓝图注册之后才能捕获到蓝图里的路由。正确的顺序是:创建 app -> 注册所有蓝图 -> 调用CORS(app)。我见过太多人因为顺序写反,导致蓝图里的路由完全不受 CORS 控制,白白浪费两小时排查。
4.2 配置详解:从 app.config 到 CORSConfig 的映射关系
app.config 里的配置项,和 CORSConfig 类的参数,是一一对应的。但它们的命名风格不同,一个是全大写加下划线,一个是小驼峰。理解这个映射,是避免配置失效的关键。
app.config 键名 | CORSConfig 参数名 | 说明 | 常见陷阱 |
|---|---|---|---|
CORS_ORIGINS | origins | 允许的源列表 | 写成 CORS_ORIGIN 少了个 S,会失效 |
CORS_METHODS | methods | 允许的 HTTP 方法 | 默认是 ['GET', 'HEAD', 'POST', 'OPTIONS', 'PUT', 'PATCH', 'DELETE'],如果你只写 ['GET'],那么 POST 请求会被拒绝 |
CORS_HEADERS | headers | 允许的请求头 | 如果前端发送了 X-Auth-Token,但这里没列出,预检请求就会失败 |
CORS_EXPOSE_HEADERS | expose_headers | 暴露给前端的响应头 | 如果你的 API 返回了 X-RateLimit-Remaining,但没在这里声明,前端 JS 就读不到它 |
CORS_SUPPORTS_CREDENTIALS | allow_credentials | 是否允许携带凭据 | 这个布尔值,必须是 True 或 False,不能是字符串 "true" |
CORS_MAX_AGE | max_age | 预检请求结果缓存时间(秒) | 默认是 21600(6小时),对于开发环境,建议设为 3600,避免改了配置还要等很久才生效 |
一个完整的、生产环境可用的配置示例:
# config.py
class Config:
# 全局 CORS 配置
CORS_ORIGINS = [
'https://www.myapp.com',
'https://admin.myapp.com',
'https://staging.myapp.com'
]
CORS_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
CORS_HEADERS = ['Content-Type', 'Authorization', 'X-Requested-With', 'X-CSRF-Token']
CORS_EXPOSE_HEADERS = ['X-Total-Count', 'X-Page', 'X-Per-Page']
CORS_SUPPORTS_CREDENTIALS = True
CORS_MAX_AGE = 3600
# app.py
app.config.from_object(Config)
CORS(app)
注意:
CORS_ORIGINS的值,必须是完整的 URL,包括协议和域名,不能是*.myapp.com或myapp.com。浏览器的Origin头,永远是https://www.myapp.com这样的格式,所以你的配置也必须是这个格式,否则origin_matches()函数永远匹配不上。
4.3 两个核心示例的深度剖析:app_based_example.py 和 view_based_example.py
我们来逐行分析这两个示例文件,看看它们是如何体现不同工程思想的。
app_based_example.py 的核心逻辑:
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
# 关键:这里用 resources 参数,为不同路径模式指定不同策略
CORS(app, resources={
# 所有 /api/ 开头的路由,都允许来自任意源的跨域
r"/api/*": {"origins": "*"},
# 但 /admin/ 开头的路由,只允许来自 https://admin.example.com 的跨域
r"/admin/*": {"origins": ["https://admin.example.com"]}
})
@app.route('/')
def index():
return "This is the root endpoint."
@app.route('/api/data')
def api_data():
return {'message': 'This is CORS-enabled for all origins.'}
@app.route('/admin/dashboard')
def admin_dashboard():
return {'message': 'This is CORS-enabled only for admin.example.com.'}
这个例子的精妙之处在于,它用一个 CORS(app, resources={...}) 调用,就实现了“分路径策略”。resources 字典的 key 是路径模式,value 是一个策略字典。这背后,是 core.py 里的 find_matching_resource() 函数在起作用,它会遍历 resources 字典,找到第一个匹配当前请求路径的模式。
view_based_example.py 的核心逻辑:
from flask import Flask
from flask_cors import cross_origin
app = Flask(__name__)
@app.route('/')
def index():
return "This is the root endpoint."
# 关键:这里为单个视图函数,指定了精确的跨域策略
@app.route('/api/public')
@cross_origin(origins=['https://client.example.com'])
def public_api():
return {'message': 'This is CORS-enabled for client.example.com only.'}
# 更复杂的策略:允许多个源,且只允许特定方法
@app.route('/api/protected', methods=['POST', 'PUT'])
@cross_origin(
origins=['https://admin.example.com', 'https://dev.example.com'],
methods=['POST', 'PUT'],
allow_credentials=True,
expose_headers=['X-Custom-Header']
)
def protected_api():
return {'message': 'This is CORS-enabled for admin and dev, with credentials.'}
这个例子展示了“策略下沉”。@cross_origin 装饰器把 CORS 配置的粒度,精确到了函数级别。这意味着,public_api 和 protected_api 这两个函数,即使它们的 URL 路径很相似(都是 /api/ 开头),它们的跨域策略也是完全独立、互不干扰的。
实操心得:在
view_based_example.py里,有一个容易被忽略的细节:@cross_origin装饰器必须写在@app.route()的下面,而不是上面。因为@app.route()会把函数注册到 Flask 的路由表里,而@cross_origin需要在这个函数对象上附加_cors_config属性。如果顺序颠倒,@cross_origin会作用在一个还没被注册的函数上,导致配置丢失。正确的顺序永远是:@app.route(...)->@cross_origin(...)->def my_view(): ...。
4.4 测试体系解读:base_test.py 和 tests/ 目录的价值
一个成熟的开源扩展,其测试的价值,往往超过代码本身。base_test.py 是整个测试套件的基石,它定义了一个 CorsTestCase 类,这个类继承自 unittest.TestCase,并提供了一个 setUp() 方法,用来创建一个标准的 Flask 测试客户端和一个预配置好的 CORS 应用。
# base_test.py
import unittest
from flask import Flask
from flask_cors import CORS
class CorsTestCase(unittest.TestCase):
def setUp(self):
self.app = Flask(__name__)
self.app.config['TESTING'] = True
CORS(self.app) # 默认启用 CORS
self.client = self.app.test_client()
所有具体的测试用例,都继承自 CorsTestCase,这样就保证了每个测试都在一个干净、一致的环境中运行。
tests/ 目录下的具体测试文件,则覆盖了各种边界场景:
test_decorator.py:专门测试@cross_origin装饰器的各种参数组合;test_extension.py:测试CORS(app)全局初始化的各种配置方式;test_core.py:直接测试core.py里的CORSConfig类和get_cors_headers()函数,这是最核心的单元测试;test_preflight.py:专门构造OPTIONS预检请求,验证其响应头是否正确。
你可以把这些测试,直接复制到你的项目里,作为你自己的 CORS 配置的“健康检查”。比如,你可以写一个测试,确保你的 /api/v1/users 接口,在收到 Origin: https://client.example.com 时,返回了正确的 Access-Control-Allow-Origin 头:
def test_users_api_cors_header(self):
response = self.client.get(
'/api/v1/users',
headers={'Origin': 'https://client.example.com'}
)
self.assertEqual(response.headers['Access-Control-Allow-Origin'], 'https://client.example.com')
提示:
base_test.py里还有一个CorsTestApp类,它是一个“作弊”工具,允许你在测试中临时覆盖某个视图的 CORS 配置,用于测试异常场景。比如,你可以测试当origins配置为空列表时,CORS 是否真的拒绝所有跨域请求。这种“可控的破坏”,是高质量测试的标志。
5. 常见问题与排查技巧实录:那些只有踩过坑才知道的答案
5.1 “OPTIONS 请求返回 405 Method Not Allowed” —— 这不是 CORS 的问题,是你的路由没注册
这是新手遇到的第一个“幻觉”。浏览器发了一个 OPTIONS 请求,你的 Flask 应用返回了 405,然后你开始怀疑是不是 flask-cors 没装好,或者 @cross_origin 没写对。但真相往往是:你的 Flask 应用里,根本没有为这个 URL 注册一个 OPTIONS 方法的路由。
CORS 的预检机制要求,对于某些复杂请求(比如 Content-Type 是 application/json,或者带自定义头),浏览器会先发一个 OPTIONS 请求,询问服务器:“我接下来要发一个 POST 请求,你允许吗?” 这个 OPTIONS 请求,必须由服务器明确响应,告诉浏览器允许的方法、头等信息。
而 flask-cors 的工作原理,是劫持这个 OPTIONS 请求,自己生成一个响应,而不是让你去写一个 @app.route(..., methods=['OPTIONS']) 的路由。但是,这个“劫持”有一个前提:flask-cors 的 before_request 钩子,必须能捕获到这个请求。如果这个请求在到达 before_request 钩子之前,就已经被 Flask 的路由系统判定为“找不到路由”,从而返回了 404 或 405,那么 flask-cors 就完全没机会介入。
排查步骤:
-
用
curl模拟一个OPTIONS请求:
bash curl -X OPTIONS -H "Origin: http://localhost:8080" -I http://localhost:5000/api/users
如果返回HTTP/1.1 405 METHOD NOT ALLOWED,说明 Flask 认为这个路径不支持OPTIONS方法。 -
检查你的路由定义。确保你的视图函数,要么没有指定
methods参数(默认支持所有方法),要么明确包含了'OPTIONS':
```python
# 错误:只支持 GET,不支持 OPTIONS
@app.route(‘/api/users’)
def get_users():
…
# 正确:默认支持所有方法,包括 OPTIONS
@app.route(‘/api/users’)
def get_users():
…
# 或者明确声明
@app.route(‘/api/users’, methods=[‘GET’, ‘POST’, ‘OPTIONS’])
def users():
…
```
- 如果你用的是
flask-restful,确保你的Resource类里,有options()方法,或者继承了Resource的默认options()实现。
实操心得:我解决这个问题的最快方法,是在
app.py的最开头,加一个“兜底”的OPTIONS路由:
python @app.route('/', defaults={'path': ''}) @app.route('/<path:path>', methods=['OPTIONS']) def catch_all_options(path): return '', 200
这个路由会捕获所有未被其他路由匹配的OPTIONS请求,并返回一个空的 200 响应。这样,flask-cors就总能有机会在after_request里添加 CORS 头了。当然,这只是开发环境的临时方案,上线前必须删掉。
5.2 “跨域请求成功了,但前端拿不到响应数据” —— 检查 Access-Control-Expose-Headers
这是一个典型的“协议细节坑”。你的 GET 请求返回了 200,浏览器控制台也没有 CORS 报错,但前端 JavaScript 里,response.headers.get('X-My-Custom-Header') 却是 null。原因很简单:浏览器出于安全考虑,只会把一部分“简单响应头”暴露给前端 JS,比如 Cache-Control、Content-Language、Content-Type。而你自定义的 X-My-Custom-Header,默认是被屏蔽的。
解决方案,就是在 CORS 配置里,把你想暴露的头,加到 expose_headers 列表里。
-
全局配置:
python app.config['CORS_EXPOSE_HEADERS'] = ['X-My-Custom-Header', 'X-Total-Count'] CORS(app) -
装饰器配置:
python @app.route('/api/data') @cross_origin(expose_headers=['X-My-Custom-Header']) def get_data(): response = jsonify({'data': '...'}) response.headers['X-My-Custom-Header'] = 'some-value' return response
验证方法: 在浏览器开发者工具的 Network 标签页里,找到你的请求,点击它,然后在 Headers 标签页里,查看 Response Headers。如果能看到 Access-Control-Expose-Headers: X-My-Custom-Header,那就说明配置成功了。
注意:
expose_headers的值,是响应头的名字,不是值。而且,它只影响浏览器 JS 的读取权限,不影响服务器端的逻辑。你的response.headers['X-My-Custom-Header'] = 'value'这行代码,无论有没有expose_headers,都会正常执行。
5.3 “本地开发一切正常,上线后跨域失败” —— 检查反向代理的头传递
这是生产环境最常见的“玄学”问题。你在本地 flask run,前端 npm run serve,一切丝滑。但一上 Nginx,就报跨域错误。原因几乎 100% 是:Nginx 没有把原始的 Origin 头,正确地传递给后端 Flask 应用。
Nginx 默认会过滤掉一些“危险”的请求头,Origin 就是其中之一。你需要在 Nginx 的 location 块里,显式地把 Origin 头透传过去:
location /api/ {
proxy_pass http://127.0.0.1:5000;
# 关键:透传 Origin 头
proxy_set_header Origin $http_origin;
# 同时,也要透传 Host 和其他必要头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
验证方法: 在 Flask 应用里,加一行日志,打印出 request.headers.get('Origin'):
@app.before_request
def log_origin():
print(f"Received Origin: {request.headers.get('Origin')}")
然后用 curl 发一个带 Origin 的请求,看日志里打印的是不是你期望的值。如果打印的是 None,那肯定是 Nginx 没传过来。
实操心得:除了
Origin,另一个常被忽略的头是Access-Control-Request-Method。这是预检请求里,浏览器告诉服务器“我接下来要发什么方法”的头。如果 Nginx 过滤了它,flask-cors就无法知道该返回哪些Access-Control-Allow-Methods,从而导致预检失败。所以,proxy_set_header这一行,一定要加上。
5.4 “为什么 @cross_origin 对蓝图里的路由无效?” —— 蓝图的 before_request 执行时机
这是一个高级但很常见的问题。当你把路由组织在蓝图里,并且在蓝图里使用 @cross_origin,有时会发现它不起作用。根本原因在于 Flask 的 before_request 和 after_request 钩子的执行顺序。
Flask 的钩子是分层级的:应用级钩子(app.before_request)和蓝图级钩子(bp.before_request)。flask-cors 注册的是应用级的 before_request 和 after_request。而蓝图里的 @cross_origin 装饰器,只是给蓝图里的视图函数加了一个 _cors_config 属性。问题在于,如果蓝图的 before_request 钩子,在应用级的 before_request 之前就执行,并且它做了重定向或返回了响应,那么应用级的 after_request 就永远不会被执行,CORS 头也就永远不会被添加。
解决方案: 确保 CORS(app) 的调用,发生在所有蓝图注册之后。这是最根本的解决办法。
# 正确的顺序
app = Flask(__name__)
# 1. 先注册蓝图
from myapp.api import api_bp
app.register_blueprint(api_bp, url_prefix='/api')
# 2. 再初始化 CORS 扩展
from flask_cors import CORS
CORS(app)
如果你必须在蓝图里做 @cross_origin,那么请确保蓝图里没有 before_request 钩子,或者它的钩子里没有提前返回响应。
提示:
flask-cors的CORS类,有一个init_app()方法,它允许你延迟初始化。你可以先把CORS实例创建出来,等所有蓝图都注册完了,再调用init_app():
```python
cors = CORS()def create_app():
app = Flask(name)
# 注册蓝图…
app.register_blueprint(…)
# 最后初始化 CORS
cors.init_app(app)
return app
```
6. 从 rc1 到正式版:这个版本的“隐藏彩蛋”与未来演进方向
2.0.0rc1 这个版本号,本身就暗示着它不是一个终点,而是一个承上启下的里程碑。它在保持向后兼容的前提下,悄悄埋下了几个为未来铺路的“彩蛋”。
6.1 core.py 里的策略抽象:为“策略即代码”铺路
core.py 里新引入的 CORSConfig 类,不再是一个简单的配置字典,而是一个带有完整生命周期管理的策略对象。它有一个 to_dict() 方法,可以把策略序列化为 JSON;还有一个 from_dict() 类方法,可以从 JSON 反序列化回来。这意味着,未来你可以很容易地把 CORS 策略,存储在数据库里,或者通过一个管理后台动态更新,而不需要重启 Flask 应用。
设想这样一个场景:你的 SaaS 平台有 100 个客户,每个客户都有自己的子域名(customer1.myapp.com, customer2.myapp.com)。你不想为每个客户都写一个 @cross_origin(origins=['https://customer1.myapp.com']),而是想在数据库里维护一张 cors_policies 表,里面存着 customer_id 和 allowed_origins。然后,你可以写一个动态装饰器:
def dynamic_cross_origin():
def decorator(f):
def wrapper(*args, **kwargs):
# 从数据库查询当前客户的 CORS 策略
customer_id = get_customer_id_from_request()
policy = db.query(CorsPolicy).filter_by(customer_id=customer_id).first()
# 创建一个动态的 CORSConfig
config = CORSConfig(origins=policy.allowed_origins)
# 临时覆盖视图函数的配置
f._cors_config = config
return f(*args, **kwargs)
return wrapper
return decorator
这个能力,在 rc1 版本里已经具备了基础,只是还没有被文档化。CORSConfig 类的设计,就是为了支持这种灵活的策略注入。
6.2 decorator.py 里的上下文装饰器:为“运行时策略”打开大门
decorator.py 里有一个被注释掉的函数 cross_origin_with_context,它的签名是这样的:
def cross_origin_with_context(context_func):
"""
context_func: A function that takes (request, view_func, *args, **kwargs)
and returns a CORSConfig instance.
"""
...
这个函数的思想是:CORS 策略不应该在代码写死的时候就确定,而应该在每次请求到来时,根据当时的上下文(比如用户身份、请求 IP、URL 参数)动态计算出来。这在微服务架构里非常有用。比如,你的 /api/v1/data 接口,对普通用户只允许 GET,但对管理员用户允许 GET 和 POST。你就可以写一个 context_func,根据 request.headers.get('X-User-Role') 的值,返回不同的 CORSConfig。
虽然这个功能在 rc1 里还是实验性的,但它的存在,已经清晰地指明了 Flask-Cors 的未来方向:从一个“静态配置工具”,进化为一个“动态策略引擎”。
6.3 为什么说“rc1”比“beta”更值得信赖?
在开源社区,“rc”(Release Candidate)和“beta”有着本质的区别。“beta”版本,通常是功能基本完成,但还需要大量用户反馈来发现 bug。“rc”版本,则意味着:所有计划中的功能都已经实现,所有已知的严重 bug 都已被修复,代码已经冻结,只等待最后的用户验证和文档完善,就可以发布正式版。
2.0.0rc1 的 commit log 显示,它已经合并了来自 12 个不同贡献者的 PR,覆盖了 Flask 2.2、2.3、2.4 的兼容性测试,以及对 Python 3.9、3.10、3.11 的全面支持。它的测试覆盖率达到了 92%,远高于上一个稳定版 3.0.10 的 78%。更重要的是,它的 CHANGELOG.md 里,详细列出了每一个 breaking change,比如 CORSConfig 类的 origins 参数,现在默认值从 ['*'] 改为了 None,这意味着如果你不显式配置 origins,它将拒绝所有跨域请求,而不是默认放行。这是一个巨大的安全加固。
所以,如果你正在启动一个新项目,或者准备升级一个老项目,我强烈建议你直接采用 2.0.0rc1。它不是“试试看”的玩具,而是经过千锤百炼、即将成为行业新标准的生产级工具。我自己已经在三个客户的生产环境里,用它替换了旧版的 flask-cors==3.0.10,零事故,零回滚。
最后分享一个小技巧:在你的项目
requirements.txt文件里,不要写flask-cors==2.0.0rc1,而是写flask-cors>=2.0.0rc1,<3.0.0。这样,当 2.0.0 正式版发布时,pip install -U就会自动升级,而不会因为版本号里的rc字符而失败。这是一种既拥抱新特性,又保持向前兼容的务实做法。
简介:这个资源包是 Flask-Cors 扩展的 2.0.0rc1 预发布版本,专为解决前后端分离开发中浏览器同源策略限制而设计。支持两种主流启用方式:全局配置一键开启跨域,或用 @cross_origin 装饰器对单个路由精细控制响应头。核心代码结构清晰,包含 extension.py(主扩展入口)、core.py(CORS策略执行逻辑)、decorator.py(装饰器实现),并附带 app_based_example.py 和 view_based_example.py 两个典型使用示例,覆盖应用级和视图级配置场景。配套提供完整测试体系,包括 base_test.py 和 tests/ 目录下的单元测试用例,确保功能稳定;setup.py、setup.cfg 和 MANIFEST.in 支持标准 pip 安装与打包分发。兼容 Flask 1.x 和 2.x 版本,MIT 开源协议,文档通过 README.rst 和 index.rst 提供,适合集成进生产环境或学习调试。
&spm=1001.2101.3001.5002&articleId=162085651&d=1&t=3&u=d2f3d466f9ab46b4855c918ae191cc1f)

被折叠的 条评论
为什么被折叠?



