跨域 CORS 原因分析及扩展

跨域 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;
  }
相关推荐
被程序耽误的胡先生1 分钟前
java中 kafka简单应用
java·开发语言·kafka
F202269748613 分钟前
Spring MVC 对象转换器:初级开发者入门指南
java·spring·mvc
楠枬34 分钟前
网页五子棋——对战后端
java·开发语言·spring boot·websocket·spring
YXWik61 小时前
23种设计模式
java·设计模式
不修×蝙蝠1 小时前
Tomcat理论(Ⅰ)
java·服务器·java-ee·tomcat
曲奇是块小饼干_1 小时前
leetcode刷题记录(一百零八)——322. 零钱兑换
java·算法·leetcode·职场和发展
hong_zc1 小时前
SpringBoot 配置文件
java·spring boot·后端
神马都会亿点点的毛毛张1 小时前
【Docker教程】万字长文详解Docker命令
java·运维·后端·docker·容器
白初&1 小时前
shiro代码层面追踪
java·shiro·代码审计·反序列化