跨域问题在前端与网关层的协作中出现的频率极高,尤其是业务前面挂了多层Nginx代理之后,浏览器明明发了带Origin的请求,后端返回的Access-Control-Allow-Origin却经常对不上,或者干脆时有时无。这类问题的根源大多不在后端代码,而在代理链路上Origin头的透传与改写。本文结合日志排查和回源配置两个方向,把Nginx处理跨域请求的完整链路讲清楚,并给出可直接落地的配置模板。

一、先搞清楚Origin在代理链路中是怎么变化的
浏览器发起跨域请求时,会在请求头里带上Origin字段,标明请求来源。服务端响应时需要返回Access-Control-Allow-Origin,并且值要和请求的Origin匹配(或者是通配符*),浏览器才会放行。这个过程本身不复杂,麻烦在于中间有多层代理。
默认情况下,Nginx作为反向代理转发请求时,并不会自动把客户端的Host、Origin这些头原样传给上游,而是会用proxy_pass指向的地址重新构造部分请求头。如果第一层Nginx没有显式设置proxy_set_header Origin $http_origin,上游收到的Origin可能是空的,或者是经过改写的值。后端拿不到真实的Origin,自然无法动态回填正确的Access-Control-Allow-Origin。
另一个常见的坑是重复响应头。有些团队在第一层Nginx统一加跨域头,后端应用(比如Spring Boot)也加了一份,最终浏览器收到两个Access-Control-Allow-Origin,浏览器直接判定跨域失败,控制台报错信息类似The 'Access-Control-Allow-Origin' header contains multiple values。这种问题单看代码很难发现,必须结合日志分析。
二、回源透传Origin的标准配置方案
推荐的做法是跨域处理收敛到一层处理:要么全部由最外层Nginx统一加跨域响应头,要么由后端应用统一处理,Nginx只负责透传。下面给出最外层Nginx统一处理的完整配置:
server {
listen 80;
server_name api.ipipp.com;
location / {
# 透传客户端原始请求头到上游
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 关键:把浏览器的Origin原样回源
proxy_set_header Origin $http_origin;
proxy_pass http://backend;
}
# 统一在代理层处理跨域响应头
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
# 预检请求直接返回,不打到后端
if ($request_method = OPTIONS) {
return 204;
}
}注意几个细节。第一,当Access-Control-Allow-Credentials为true时,Allow-Origin不能写死为*,必须回显具体的Origin值,这就是为什么上面用$http_origin变量动态返回。第二,always参数很重要,它保证4xx、5xx等错误响应也带上跨域头,否则前端拿到的错误信息会被浏览器拦截,排查起来非常痛苦。第三,if ($request_method = OPTIONS)用于拦截预检请求,减少对后端的无谓打点。
如果后端自己也返回了跨域头,就会造成重复。这时可以用proxy_hide_header在上游响应到达Nginx时先隐藏掉后端加的头,再由Nginx统一添加:
location / {
proxy_set_header Origin $http_origin;
# 隐藏后端返回的跨域头,避免重复
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_pass http://backend;
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials true always;
}三、用日志定位跨域问题的实用技巧
配置改完不等于万事大吉,跨域问题往往需要看真实的请求头才能定位。Nginx默认的combined日志不包含Origin信息,需要自定义log_format把它打出来:
log_format cors '$remote_addr - $time_local "$request" '
'status=$status origin="$http_origin" '
'acao="$upstream_http_access_control_allow_origin" '
'upstream=$upstream_addr rt=$request_time';
server {
...
access_log /var/log/nginx/cors.log cors;
}这个格式里有两个关键字段。$http_origin记录浏览器发来的原始Origin,$upstream_http_access_control_allow_origin记录后端实际返回的跨域头值。两相对照,一眼就能看出是Origin没传到后端,还是后端回的头不匹配。比如日志中出现origin为空但请求正常的情况,多半说明客户端走了同域或者代理层把头丢了。
排查预检失败时还可以临时开启调试手段,用curl模拟带Origin的请求验证网关行为:
curl -i -X OPTIONS "http://api.ipipp.com/user/info" \
-H "Origin: http://www.ipipp.com" \
-H "Access-Control-Request-Method: POST"观察返回头中Access-Control-Allow-Origin是否等于你传入的Origin,是否有重复。如果curl返回正常但浏览器报错,那问题大概率出在浏览器的凭据模式(fetch带了credentials而服务端没回Allow-Credentials)或者响应链路上还有CDN、WAF等设备二次改写了头。把这些环节逐层验证,跨域问题基本都能收敛到Origin透传和响应头重复这两个原因上,配置思路与上面的模板一致即可解决。