跨域 CORS 是浏览器对非同源服务器数据的限制,但一般却需要服务端来解决。这里分享一个在特定的 iOS 版本上才报跨域错误的案例。
一、特定 iOS 版本跨域错误的场景分析
iOS12 请求报错 http status code=0。
页面 A(x.aaa.com) 调用 B 接口(y.bbb.com) 接口,iOS12 版本报错 http status code=0。请求为 content-type: application/json,header 中带自定义字段 token,iOS 更高版本正常。
请求路径为 页面A → 服务端 nginx → 服务端网关 → 具体业务接口
请求符合跨域特征,首先查看 nginx 日志,

首先发起 options 请求,nginx 正常返回 204,后续 nginx 未收到 post 请求。
查看 nginx 配置:
lua
location /xxx/ {
proxy_pass <http://k8s-ingress/xxx/>;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header HOST $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
if ($request_method = 'OPTIONS'){
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Headers * always;
add_header Access-Control-Allow-Methods 'GET,POST,PUT,DELETE,OPTIONS,PATCH' always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Max-Age 3600 always;
return 204;
}
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods 'GET,POST,PUT,DELETE,OPTIONS,PATCH' always;
add_header Access-Control-Expose-Headers * always;
}
nginx 已针对请求做跨域处理,允许所有 origin,所有 header。
在 iOS12 版本,返回的 Access-Control-Allow-Headers 为 * 没有生效(可能觉得不安全?,但是更高版本却生效了),从而导致没有继续发送 post 请求。而在安卓上,该配置生效了。
更改 nginx 配置后正常。
lua
location /xxx/ {
proxy_pass <http://k8s-ingress/xxx/>;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header HOST $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
if ($request_method = 'OPTIONS'){
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Headers $http_access_control_request_headers always;
add_header Access-Control-Allow-Methods 'GET,POST,PUT,DELETE,OPTIONS,PATCH' always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Max-Age 3600 always;
return 204;
}
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,PATCH' always;
add_header Access-Control-Expose-Headers $http_access_control_request_headers always;
}
$http_origin
:请求的源地址;
$http_access_control_request_headers
:请求带的 header。
二、为何 Spring 程序配置允许所有 header 没这个问题
如果在 Java 程序端开启 cors 配置后,iOS12 版本却返回正常,配置如下:
java
if (HttpMethod.OPTIONS.toString().equalsIgnoreCase(method)) {
response.setHeader("Access-Controller-Allow-Headers", "*");
}
查看 spring-web 相关源码,

可以发现,程序对 * 进行了处理。遍历请求头,如果 allowedHeaders 配置为 *,则返回实际请求头中的 header。
三、跨域请求时,OPTIONS 请求触发条件
1、使用了下面任一HTTP 方法:PUT/DELETE/CONNECT/OPTIONS/TRACE/PATCH;
2、人为设置了以下集合之外首部字段:Accept/Accept-Language/Content-Language/Content-Type/DPR/Downlink/Save-Data/Viewport-Width/Width;
3、Content-Type 的值不属于下列之一: application/x-www-form-urlencoded、multipart/form-data、text/plain。
一旦达到触发条件,跨域请求便会一直发送 2 次请求(一次 OPTIONS,一次实际请求),这样增加的请求数是否可优化呢?答案是可以,OPTIONS 预检请求的结果可以被缓存,通过设置 Access-Control-Max-Age
即可,单位为秒。
四、是否可以 nginx 和 Spring 同时配置允许跨域?
nginx 配置如下:
lua
location /xxx-pre/ {
...
proxy_pass http://pre:10500/;
if ($request_method = 'OPTIONS'){
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Headers' '*';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
add_header Access-Control-Allow-Credentials true;
return 200;
}
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
add_header 'Access-Control-Expose-Headers' '*';
}
程序代码如下:

nginx 允许 Origin 为 *,@CrossOrigin 默认也是允许 Origin 为 *。
调用接口,发现响应头中有 2 个 Access-Controller-Allow-Origin 字段;当出现重复的跨域头时,请求还是会被跨域策略阻止。因此,不能重复配置跨域设置。

五、跨域配置的比较
假设后端架构是 nginx → gateway → 业务微服务,那么可以配置允许跨域的地方至少有 3 个。
方式 | 范围 |
---|---|
nginx 配置 | 针对 location 指定的路径。 |
gateway 配置 | 针对所有业务微服务。 |
业务微服务 | 通过 filter 等方式处理,针对所有接口。 |
业务微服务 | 通过 @CrossOrigin 注解方式,针对注解指定的类或者方法。 |
六、多个地方同时配置跨域处理
比如 nginx 上配置了允许所有域名访问,但某个具体的接口只允许特定域名 A 访问,如何处理?
可以考虑给 nginx 增加 lua 模块,增加逻辑判断:如果相应头已经包含对应字段,比如 Access-Control-Allow-Origin,则直接返回;如果不包含,则执行 add_header 操作。
lua
location /xxx/ {
header_filter_by_lua_block {
local h = ngx.resp.get_headers()
if not h["access-control-allow-credentials"] then
ngx.header["access-control-allow-credentials"] = "true"
end
if not h["access-control-allow-origin"] then
ngx.header["access-control-allow-origin"] = "*"
end
if not h["access-control-allow-headers"] then
ngx.header["access-control-allow-headers"] = "*"
end
if not h["access-control-allow-methods"] then
ngx.header["access-control-allow-methods"] = "GET,POST,PUT,DELETE,OPTIONS,PATCH"
end
if not h["access-control-max-age"] then
ngx.header["access-control-max-age"] = "3600"
end
}
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass <http://test/xxx/>;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header HOST $host;
}