Nginx 跨域请求报 Access-Control-Allow-Origin contains multiple values
PHP 或 HTTP 上游与 Nginx 同时添加允许来源头时,浏览器会拒绝跨域响应。确认重复所在的响应,统一策略归属,并按 FastCGI 或 HTTP 转发协议处理上游头。
浏览器跨域请求已经收到 HTTP 响应,却拒绝把结果交给 JavaScript,并提示:
The 'Access-Control-Allow-Origin' header contains multiple values
'*, https://app.example.com', but only one is allowed.
即使 Nginx 配置里只找到一条 add_header Access-Control-Allow-Origin,也可能出现这个错误:PHP 应用先返回了 Access-Control-Allow-Origin: *,Nginx 又追加了前端域名。add_header 不会替换应用已经生成的同名响应头。
本文处理 Nginx 转发 PHP/FastCGI 或 HTTP 上游时重复写入这个响应头的场景。应用和 Nginx 的具体版本不是判断依据;需要先在实际响应中确认重复。没有响应、证书错误或完全缺少该响应头,属于其他排查分支。
先确认重复发生在哪个响应
在浏览器开发者工具的 Network 面板中打开失败请求,查看响应头。可能看到两行:
Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: https://app.example.com
也可能被界面合并显示成一行逗号分隔的值。这个字段只能提供一个允许的 origin,或者在符合条件时使用 *,不能写成域名列表。两行即使值相同,也不能靠“重复的是同一个域名”忽略问题。
在测试环境可用以下请求查看原始响应头;把两个示例域名和接口路径换成自己的值:
curl -sS -D - -o /dev/null \
-H 'Origin: https://app.example.com' \
'https://api.example.com/geo.php'
这里使用 GET 而不是 HEAD,避免接口对两种方法走不同逻辑。curl 能显示服务器发回什么,但不执行浏览器的 CORS 检查;它成功收到正文不能证明跨域访问已经恢复。
如果浏览器先发了 OPTIONS 预检,分别检查 OPTIONS 和后续业务请求。Nginx 可能直接处理预检,PHP 处理业务请求,所以一个响应正确、另一个重复并不矛盾。不要只查看预检的 204 状态就结束排查。
让应用或 Nginx 中的一层负责生成
检查实际命中的 Nginx server、location 及其引用的配置,再检查应用入口和 CORS 中间件。PHP 中类似下面的语句会生成这个响应头:
header('Access-Control-Allow-Origin: *');
若 Nginx 已经维护正确的允许来源策略,应用里这条历史遗留的通配头就应删除;不要再增加一条域名头来“覆盖”它。若应用框架已经统一管理 CORS,则可以保留框架策略,移除该接口经过的 Nginx 配置中额外添加的同名头。
选哪一层取决于现有部署,但需要让允许来源、凭证、请求方法和请求头的规则彼此一致。不要只为消除错误,把受限接口改成对所有来源开放。前端使用 credentials: 'include' 时,允许来源不能是 *;也不能把任意请求的 Origin 原样反射回去当作授权。
修改前保存所涉及的应用和 Nginx 配置版本,并先在测试环境验证。删除应用头以后,如果浏览器变成“缺少允许来源”,说明保留的那一层没有覆盖当前响应,可能是路由、状态码或配置作用域不同。此时应修复该层的覆盖范围,不要把刚删掉的重复头再加回来。
无法修改上游时,按转发协议隐藏上游头
有些第三方应用无法关闭自带的头。只有在 Nginx 已有正确、完整的 CORS 策略时,才考虑让 Nginx 不转发上游的同名字段。
PHP 通过 fastcgi_pass 交给 PHP-FPM 时,在现有处理该接口的 FastCGI location 内加入:
fastcgi_hide_header Access-Control-Allow-Origin;
保留这个 location 原有的 fastcgi_pass、参数和已经核定的允许来源配置。这条指令只隐藏 FastCGI 响应中的字段,不负责生成新的允许来源,也不删除 Nginx 自己通过 add_header 添加的字段。
如果后端通过 proxy_pass 以 HTTP 协议转发,对应地在那个现有 location 内加入:
proxy_hide_header Access-Control-Allow-Origin;
两段是按上游协议选择的局部修改,不是可替换整个站点的配置。PHP-FPM 场景套用 proxy_hide_header 无法过滤 FastCGI 返回的头;反过来也一样。不要把隐藏规则直接放到全站,误伤由应用独立维护 CORS 的其他接口。
如果重复来自两条 Nginx add_header,或者来自 Nginx 后面的另一层网关,这两个隐藏上游头的指令也不能代替清理相应配置。应沿请求路径确认每一层实际添加了什么。
验证时同时检查允许和拒绝的来源
按现有部署流程校验配置并重新加载后,再从浏览器发起同一请求。允许的来源应只收到一个正确的 Access-Control-Allow-Origin 值;需要预检时,预检和实际响应都要符合策略。测试一个不在允许名单里的来源,确认它没有因为这次修改获得跨域读取权限。
同时覆盖成功响应与接口正常产生的错误响应。Nginx add_header 默认只在指定状态码上添加头;always 参数从 1.7.5 起可用于其他状态码,但是否采用应由接口策略决定,它不会解决重复头问题。不要为了让错误信息可见而顺手扩大允许来源。
如果链路有缓存,确认读取到的是新响应;仅修改配置却反复查看旧缓存,会误判修复无效。回退时恢复本次修改的应用或 Nginx 配置版本,并再次检查响应头。恢复旧配置也可能恢复原来的重复问题,因此回退只能用于撤销本次变更带来的额外影响,不能作为问题已经解决的依据。


