Apache APISIX cors 插件详解:跨域资源共享配置、高级匹配策略与源码实现原理
【免费下载链接】apisixThe Cloud-Native API Gateway项目地址: https://gitcode.com/GitHub_Trending/ap/apisix
本指南围绕 Apache APISIX 云原生 API 网关中的cors插件展开,系统讲解如何通过该插件在 Route/Service 上快速开启 CORS(跨域资源共享),覆盖全部属性参数、allow_credential与**强制通配符的安全边界、正则与元数据(Metadata)两种高级 Origin 匹配方式,并结合 插件源码 与 测试用例 剖析其底层实现原理。读完本文,你将能够独立完成从"一行配置开启 CORS"到"精细化白名单管控 + 安全加固"的完整落地。
CORS 与 APISIX cors 插件
跨域资源共享(CORS)是浏览器基于 HTTP 头实现的一种安全机制:当页面所在域(Origin)与请求目标域不一致时,浏览器会先发起 OPTIONS 预检请求(Preflight),并依据服务器返回的Access-Control-*系列响应头判断是否允许该跨域请求继续执行。传统方案需要在每个后端服务里重复编写 CORS 逻辑,而 APISIX 提供的cors插件可以在网关层统一、集中地处理所有跨域场景,后端服务无需任何改动。
该插件完整实现了 CORS 规范中的Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers、Access-Control-Expose-Headers、Access-Control-Max-Age、Access-Control-Allow-Credentials响应头,并额外支持 Resource Timing API 的Timing-Allow-Origin响应头,可直接挂载到 Route 或 Service 上(从源码看,插件定义位于 apisix/plugins/cors.lua,插件优先级priority = 4000)。
属性配置总览
cors插件的属性分为两类:CORS 属性与 Resource Timing 属性。
CORS 属性
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| allow_origins | string | 否 | "*" | 允许跨域的 Origin,格式为scheme://host:port,例如https://somedomain.com:8081。多个 Origin 用英文逗号,分隔。当allow_credential为false时可用*表示允许所有来源;当allow_credential为true时可用**强制允许所有来源,但会带来安全风险 |
| allow_methods | string | 否 | "*" | 允许跨域的请求方法,例如GET,POST,多个方法用逗号分隔。*与**的语义同上 |
| allow_headers | string | 否 | "*" | 允许的请求头,多个头用逗号分隔。*与**的语义同上 |
| expose_headers | string | 否 | 无 | 允许浏览器读取的响应头,多个头用逗号分隔。未指定时插件不会修改Access-Control-Expose-Headers响应头 |
| max_age | integer | 否 | 5 | 预检结果被浏览器缓存的最大秒数,缓存期内浏览器直接使用缓存结果;设置为-1表示禁用缓存。注意最大值受浏览器实现限制 |
| allow_credential | boolean | 否 | false | 置为true时允许请求携带 Cookie 等凭据。根据 CORS 规范,此选项为true时其他属性不能使用*通配 |
| allow_origins_by_regex | array | 否 | nil | 通过正则匹配允许跨域的 Origin,例如[".*\.test.com$"]可匹配test.com的所有子域。一旦设置,仅命中该正则范围的域名会被放行,allow_origins不再参与判断 |
| allow_origins_by_metadata | array | 否 | nil | 引用插件 Metadata 中allow_origins映射的键来允许跨域。例如 Metadata 中配置了"allow_origins": {"EXAMPLE": "https://example.com"},则此处填["EXAMPLE"]即可放行https://example.com |
Resource Timing 属性
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| timing_allow_origins | string | 否 | nil | 允许访问资源计时信息的 Origin,格式为scheme://host:port,例如https://somedomain.com:8081,多个 Origin 用逗号分隔。对应响应头Timing-Allow-Origin |
| timing_allow_origins_by_regex | array | 否 | nil | 通过正则匹配允许访问资源计时信息的 Origin,例如[".*\.test.com"]可匹配test.com的所有子域。一旦设置,仅命中该正则范围的域名会被放行,timing_allow_origins不再参与判断 |
两条重要安全约束
allow_credential属性非常敏感,必须谨慎使用。若置为true,其他属性默认的*值将失效,必须显式指定具体值。- 使用
**强制通配符时,你将暴露于 CSRF 等安全风险之下,使用前请确认它满足你的安全级别要求。
这两条规则并非仅写在文档中,在 插件源码的check_schema校验逻辑 里被硬性执行:当allow_credential为true时,若allow_origins、allow_methods、allow_headers、expose_headers、timing_allow_origins任一为"*",校验直接返回错误you can not set '*' for other option when 'allow_credential' is true。对应地,cors 测试用例 中的 TEST 24~27 验证了这五种组合均会被 Admin API 以 400 拒绝。同时,源码使用正则^(\*|\*\*|null|\w+://[^,]+(,\w+://[^,]+)*)$(cors.lua 第 26 行)校验 Origin 格式,cors2 测试 覆盖了合法值(*、**、null、带端口的 URL 列表等)与非法值(如*a、x.com、缺协议头的http//y.com.uk等)的完整矩阵。
理解 Timing-Allow-Origin 的适用场景
Timing-Allow-Origin响应头定义于 Resource Timing API,但与 CORS 概念紧密相关。设想你有domain-A.com和domain-B.com两个域名:你正停留在domain-A.com的页面上,通过 XHR 请求domain-B.com上的资源,并且需要读取该请求的计时信息(如PerformanceResourceTiming)。只有当你在domain-B.com上拥有跨域权限时,浏览器才会向你展示这部分计时信息。
因此正确步骤是:先配置好 CORS 响应头,再访问domain-B.com的 URL,同时设置Timing-Allow-Origin,浏览器才会返回所请求的计时数据。从 cors4 测试用例 可以看出,allow_origins与timing_allow_origins是两套相互独立的放行集合——Origin 只命中allow_origins时仅返回 CORS 头(如 TEST 14),只命中timing_allow_origins时仅返回Timing-Allow-Origin(如 TEST 19),两者均命中才同时返回两组响应头(如 TEST 17)。
插件 Metadata:全局共享的 Origin 白名单
cors插件还支持 Metadata 配置,其 schema 定义了一个allow_origins对象(见 cors.lua 第 35-46 行):
| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| allow_origins | object | 否 | Origin 引用名与允许来源的映射表。映射的键用于插件属性allow_origins_by_metadata,映射的值语义与插件属性allow_origins完全一致 |
Metadata 的价值在于一处定义、多处引用:把跨多个 Route 复用的 Origin 白名单集中维护在插件级 Metadata 中,Route 侧只需通过键名引用,避免在每个 Route 里重复书写冗长的 Origin 列表。从源码看,process_with_allow_origins_by_metadata通过plugin.plugin_metadata(plugin_name)读取元数据,并逐个键进行 Origin 匹配;cors3 测试 展示了 Metadata 的典型配置形态,例如:
{ "allow_origins": { "key_1": "https://domain.com", "key_2": "https://sub.domain.com,https://sub2.domain.com", "key_3": "*" } }需要说明的是,当 Route 上同时配置了allow_origins_by_metadata与allow_origins时,后者的*通配在元数据匹配场景下是无效的(cors3 测试 TEST 13-14 验证了这一行为),即元数据匹配优先于普通列表匹配。
启用插件
cors插件默认处于启用状态,可以直接在指定 Route 或 Service 上挂载。以下命令在路由/hello上以全默认参数开启 CORS。
首先从config.yaml中取出admin_key并存入环境变量(Admin API 的鉴权配置见 conf/config.yaml,生产环境请务必更换为强随机密钥):
admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g')然后通过 Admin API 创建路由并启用插件:
curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -d ' { "uri": "/hello", "plugins": { "cors": {} }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:8080": 1 } } }'插件启用后无需重启 APISIX,配置经由 etcd 下发后即实时生效。
验证默认效果
启用插件后,向网关发起请求即可在响应中看到 CORS 响应头:
curl http://127.0.0.1:9080/hello -v... < Server: APISIX web server < Access-Control-Allow-Origin: * < Access-Control-Allow-Methods: * < Access-Control-Allow-Headers: * < Access-Control-Max-Age: 5 ...这与 cors 测试用例 TEST 7 断言的默认行为完全一致:默认配置下Access-Control-Allow-Origin、Allow-Methods、Allow-Headers均为*,Access-Control-Max-Age为 5 秒。注意默认情况下不会输出Access-Control-Expose-Headers(源码中仅当expose_headers非空时才设置该头,见 set_cors_headers)。
预检请求(OPTIONS)的处理
插件在rewrite阶段(cors.lua 第 331-337 行)对 OPTIONS 请求做了短路处理:直接返回 200 并终止后续流程,从而把 CORS 预检请求拦截在网关层,避免其打到上游后端(测试 TEST 14 验证了OPTIONS /hello直接返回空响应体)。这也是该插件能够"后端零改动"实现 CORS 的关键设计之一。
进阶配置场景
场景一:指定来源与凭据(allow_credential)
当跨域请求需要携带 Cookie 时,必须开启allow_credential并显式列出允许的来源与方法:
curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -d ' { "uri": "/hello", "plugins": { "cors": { "allow_origins": "http://sub.domain.com,http://sub2.domain.com", "allow_methods": "GET,POST", "allow_headers": "headr1,headr2", "expose_headers": "ex-headr1,ex-headr2", "max_age": 50, "allow_credential": true } }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:8080": 1 } } }'发起携带Origin头的请求后(对应 测试 TEST 9):
curl http://127.0.0.1:9080/hello -H "Origin: http://sub2.domain.com" -v响应头应包含:
< Access-Control-Allow-Origin: http://sub2.domain.com < Access-Control-Allow-Methods: GET,POST < Access-Control-Allow-Headers: headr1,headr2 < Access-Control-Expose-Headers: ex-headr1,ex-headr2 < Access-Control-Max-Age: 50 < Access-Control-Allow-Credentials: true同时会额外输出Vary: Origin响应头(源码见 cors.lua 第 376-378 行),这是 CORS 的最佳实践——告知缓存系统响应内容随Origin变化,避免代理/浏览器缓存把 A 域名的 CORS 头错误复用到 B 域名。若请求的Origin不在白名单中,则不会输出任何 CORS 头(测试 TEST 10 验证了该行为),浏览器将按 CORS 规范拦截响应。
场景二:强制通配(**)与请求头回显
当allow_credential为false时,可以使用**强制放行所有来源/方法/请求头。与*不同的是,**会动态回显真实的来源:Access-Control-Allow-Origin直接返回请求的Origin值(无 Origin 时回退为*);Access-Control-Allow-Methods被展开为GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE全量方法列表(set_cors_headers 中的展开逻辑);Access-Control-Allow-Headers则回显预检请求携带的Access-Control-Request-Headers头(cors.lua 第 230-235 行),测试 TEST 12 完整呈现了这套行为:
< Access-Control-Allow-Origin: https://sub.domain.com < Access-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE < Access-Control-Allow-Headers: req-header1,req-header2 < Access-Control-Max-Age: 5场景三:正则匹配子域
当允许的域名规模较大或动态变化时,可用allow_origins_by_regex以正则批量匹配(测试 TEST 28-33 覆盖了单正则与多正则场景):
curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -d ' { "uri": "/hello", "plugins": { "cors": { "allow_origins": "http://sub.domain.com,http://sub2.domain.com", "allow_methods": "GET,POST", "allow_headers": "headr1,headr2", "expose_headers": "ex-headr1,ex-headr2", "max_age": 50, "allow_credential": true, "allow_origins_by_regex": [".*\\.test.com$", ".*\\.example.org$"] } }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:8080": 1 } } }'正则模式下,http://a.test.com、http://foo.example.org等命中规则即被放行,且Access-Control-Allow-Origin回显请求来源;未命中的http://a.test2.com等则拿不到任何 CORS 头。正则规则在配置校验阶段就会预编译(cors.lua 第 196-212 行),非法正则无法通过 Admin API 提交。
源码级原理剖析
执行阶段与优先级
cors插件定义了priority = 4000的执行优先级(cors.lua 第 147-153 行),并分别在两个阶段介入请求生命周期:
- rewrite 阶段(
_M.rewrite):保存原始Origin请求头到ctx.original_request_origin,并短路拦截 OPTIONS 预检请求。 - header_filter 阶段(
_M.header_filter):依据放行策略计算并写入 CORS 与 Timing 相关响应头。
之所以要在 rewrite 阶段提前保存原始 Origin,是为了抵御其他插件(如 proxy-rewrite)在后续阶段改写Origin请求头:测试 TEST 34-35 验证了即使上游重写插件把Origin改为http://example.com,CORS 匹配依然以客户端真实来源为准。
多 Origin 列表的缓存优化
当allow_origins包含逗号分隔的多个来源时,插件通过 create_multiple_origin_cache 把列表拆分为哈希表,并借助core.lrucache(type = "plugin"的插件级缓存)进行缓存,避免每个请求都重复执行字符串分割与正则迭代(cors.lua 第 254-263 行)。
匹配优先级
从header_filter的控制流可以归纳出 Origin 匹配的完整优先级链:
- 若配置了
allow_origins_by_metadata,优先按元数据映射匹配; - 若配置了
allow_origins_by_regex,则只用正则判断,普通allow_origins列表被跳过; - 否则使用
allow_origins列表做精确匹配或*通配(req_origin == allow_origins or allow_origins == '*',见 match_origins); - 全部未命中时,再回退尝试一次元数据匹配。
正则与 Timing 相关规则遵循同一套优先级逻辑(timing_allow_origins_by_regex优先于timing_allow_origins),测试 cors4 TEST 25 明确验证了"正则优先于列表"的设计意图。
覆盖上游响应头
header_filter阶段使用core.response.set_header设置响应头,因此即使上游后端自行输出了 CORS 头,也会被插件覆盖为网关策略的最终值(测试 TEST 17-23 验证了Access-Control-Allow-Origin、Allow-Methods、Allow-Headers、Expose-Headers、Max-Age、Allow-Credentials六类响应头均以上游为准被覆写)。这意味着后端可以彻底移除自身的 CORS 逻辑,由网关统一收敛。
与鉴权插件协同
得益于header_filter阶段的执行时机,即使请求因鉴权失败返回 401/403,CORS 响应头依然会被写入(cors 测试 TEST 15-16 验证了key-auth鉴权失败返回 401 的同时仍携带完整的 CORS 头)。这保证了浏览器端能正常读取错误响应,避免出现"鉴权失败但跨域报错"的双重迷惑问题。
删除插件
移除cors插件只需将对应配置从插件配置中删除即可。APISIX 会自动热加载生效,无需重启:
curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -d ' { "uri": "/hello", "plugins": {}, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:8080": 1 } } }'删除后请求将不再携带任何Access-Control-*响应头。
安全使用建议
- 避免
*与凭据混用:allow_credential: true时禁止使用*,这是 CORS 规范与插件校验的双重约束,务必为allow_origins、allow_methods、allow_headers显式列出精确值。 - 慎用
**强制通配:**会放行任意来源并回显其 Origin,使攻击者可以从任意域名发起携带 Cookie 的跨域请求,显著扩大 CSRF 攻击面;仅在完全可信的内网或公开只读接口场景下使用。 - 生产环境优先使用白名单:对于面向公网的 API,推荐组合使用精确 Origin 列表、
allow_origins_by_regex(子域批量放行)与 Metadata 复用机制,将放行面收敛到最小。 - 关注
Vary: Origin:只要allow_origins不是*,插件就会自动追加Vary: Origin,这有助于缓存系统正确区分不同来源的响应,请勿在网关层移除该头。
测试与验证资源
仓库内为cors插件提供了完善的测试覆盖,可作为配置行为的权威参考:
- t/plugin/cors.t:默认行为、指定来源、强制通配、OPTIONS 短路、鉴权协同、响应头覆盖、凭据与
*冲突校验等基础用例; - t/plugin/cors2.t:Origin 格式校验矩阵与正则匹配优先级;
- t/plugin/cors3.t:插件 Metadata 的
allow_origins映射与allow_origins_by_metadata引用; - t/plugin/cors4.t:
timing_allow_origins/timing_allow_origins_by_regex的独立放行语义。
结合 插件实现 阅读这些用例,可以快速验证任意配置组合的预期响应头,是排查线上跨域问题时的有力工具。
【免费下载链接】apisixThe Cloud-Native API Gateway项目地址: https://gitcode.com/GitHub_Trending/ap/apisix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考