最近在重构公司前端项目时,遇到了一个看似简单却让人头疼的问题:Nginx转发自定义请求头时出现的各种异常情况。这个问题在开发环境测试时一切正常,但上线后却陆续收到移动端用户反馈接口异常,PC端在某些浏览器下也出现跨域问题。经过一周的排查和修复,我整理了这份完整的踩坑记录和解决方案。
自定义请求头在现代Web开发中非常常见,比如用于传递用户认证信息的Authorization头、用于灰度发布的Version头等。但当这些自定义头经过Nginx代理时,往往会遇到各种"诡异"的问题:有的浏览器能正常工作,有的却报跨域错误;PC端正常但移动端异常;甚至同一个浏览器在不同网络环境下表现不一致。
当浏览器发起跨域请求时,会先发送一个OPTIONS预检请求(Preflight Request)来确认服务器是否允许实际请求。这个机制是CORS(跨域资源共享)规范的一部分。预检请求会包含以下两个特殊头:
只有当服务器正确响应这些预检请求后,浏览器才会发送实际的请求。而问题往往出现在Nginx没有正确配置对这些预检请求的处理。
移动端环境比PC端更加复杂,主要表现在:
我们遇到的一个典型情况是:在iOS的某些版本下,自定义头会被自动转换为小写,而Nginx配置中如果严格匹配大写就会失效。
nginx复制server {
listen 80;
server_name api.example.com;
location / {
# 处理预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,Version';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
# 处理实际请求
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,Version';
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 关键配置:确保自定义头被传递
proxy_set_header Authorization $http_authorization;
proxy_set_header Version $http_version;
}
}
Access-Control-Allow-Headers必须明确列出所有需要支持的自定义头proxy_set_header指令确保自定义头被正确传递给后端服务Access-Control-Max-Age可以减少预检请求次数,提升性能由于移动端环境复杂,建议在Nginx配置中对头名称做大小写不敏感处理:
nginx复制map $http_authorization $auth_header {
default $http_authorization;
"" $http_Authorization;
}
map $http_version $version_header {
default $http_version;
"" $http_Version;
}
server {
# ...
proxy_set_header Authorization $auth_header;
proxy_set_header Version $version_header;
}
某些运营商会过滤或修改HTTP头,可以采用以下策略:
bash复制# 测试预检请求
curl -X OPTIONS -H "Origin: http://example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: version,authorization" \
-I http://api.example.com/endpoint
# 测试实际请求
curl -X POST -H "Origin: http://example.com" \
-H "Authorization: Bearer xxx" \
-H "Version: 1.0" \
-H "Content-Type: application/json" \
-d '{"key":"value"}' \
http://api.example.com/endpoint
不同环境(开发、测试、生产)可能需要不同的CORS策略:
nginx复制# 在http块中定义变量
map $host $cors_origin {
default "*";
"~*dev\.example\.com" "http://localhost:3000";
"~*test\.example\.com" "https://test.example.com";
"~*api\.example\.com" "https://www.example.com";
}
server {
# ...
add_header 'Access-Control-Allow-Origin' $cors_origin;
# ...
}
如果需要传递大量动态头,可以使用变量:
nginx复制location / {
# 传递所有以X-开头的头
proxy_set_header X-Forwarded-Headers $http_x_forwarded_headers;
# 后端可以从X-Forwarded-Headers中解析出原始头
}
合理设置Access-Control-Max-Age可以减少预检请求:
nginx复制add_header 'Access-Control-Max-Age' 1728000; # 20天
减少不必要的头传递:
nginx复制# 合并多个头为一个
map "$http_authorization:$http_version" $combined_header {
default "";
"~*(?<auth>[^:]+):(?<ver>[^:]+)" "auth=$auth&ver=$ver";
}
server {
# ...
proxy_set_header X-Combined $combined_header;
}
Access-Control-Allow-Origin: *,应该明确指定允许的域名nginx复制# 安全示例
set $cors "";
if ($http_origin ~* (https?://(www\.)?(example\.com|example\.org))) {
set $cors $http_origin;
}
add_header 'Access-Control-Allow-Origin' $cors;
add_header 'Access-Control-Allow-Methods' 'GET, POST';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
解决方案:
nginx复制location / {
if ($request_method = 'OPTIONS') {
# ...
return 204; # 确保返回204而不是200
}
}
解决方案:
解决方案:
nginx复制# 增加缓冲区大小
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
经过这次踩坑,我深刻体会到即使是看似简单的Nginx配置,在实际生产环境中也会遇到各种边界情况。特别是在移动端,不同设备、不同浏览器、不同网络环境下的表现可能大相径庭。建议在项目初期就充分测试各种场景,避免上线后出现问题。