Nginx 跨域有关的预检请求preflight request

背景

同事有一个跨域的需求,域外html集成的js要访问Nginx反向代理的一个站点。具体HTTP方法和header 我也没问,想着就把以前其他同事配置过的跨域的一段参数拷贝过来就行了,拷贝的具体参数如下

location /crosstest/web/ {
    add_header Access-Control-Allow-Origin: * ;
    add_header Access-Control-Allow-Credentials true;
    add_header Access-Control-Allow-Methods GET,PUT,POST,DELETE,OPTIONS;
    add_header Access-Control-Allow-Headers Content-Type,* ;
    proxy_pass http://webserver/web;
}

测试过程中发现不行,浏览器会报错如下

Access to XMLHttpRequest at 'http://2.2.2.2/crosstest/web/login' from origin 'http://1.1.1.1' has been blocked by CORS policy: Response to preflight request doesn't access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource

大致意思是说从1.1.1.1 地址向2.2.2.2 发起的请求被跨域策略拦截了。因为请求的资源上不存在 ‘Access-Control-Allow-Origin’ 这个header,导致preflight request 失败。为了搞清楚客户端(源地址端)在访问Nginx的时候具体发生了啥,找同事要了前端的代码,放到本地 localhost:8080 里,自己研究下这个过程到底发生了啥。

nginx 拦截post请求 nginx拦截get请求_服务器

从Fidder 抓取的请求看,客户端仅发送了一个OPTIONS 方法的请求,被服务器403状态码给拒绝了,查阅了有关OPTIONS方法和预检请求的博客和文档,梳理了大概关系

  1. HTTP 请求分为简单请求 与 复杂请求,两种请求的区别主要在于简单请求不会触发CORS预检请求,而复杂请求会触发CORS预检请求
  2. 满足简单请求的条件(两个条件需要都满足)
  • 方法为 GET、HEAD、POST 之一
  • 无自定义请求头,且Content-Type 为 text/plain, mutipart/form-data application/x-www-form-urlencoded 之一
  1. 不满足简单请求的一切请求都是复杂请求
  2. 预检请求(一般是浏览器自动发起的OPTIONS方法的请求) 中Access-Control-Request-Method 字段告诉服务器实际请求会使用的HTTP方法;Access-Control-Request-Headers 字段告知服务器实际情况所携带的自定义首部字段。服务器基于预检请求获得的信息来判断,是否接受接下来的实际请求。服务器端返回的Access-Control-Allow-Methods 字段 将服务器允许的请求方法告诉客户端。该首部字段与Allow类似,但只能用户设计到CORS的场景中。

到这里,产生了几个疑问

为什么要发起预检请求 ?

《关于preflight request》 解释的比较清楚,目前浏览器限制跨域的方式主要有两种

  1. 浏览器限制发起跨域请求
  2. 跨域请求可以正常发起,但是返回的结果被浏览器拦截

一般浏览器都是采用第二种方式限制跨域请求,也就是说请求已经到达了服务器,如果是复杂请求,对服务器数据库的数据进行了操作,但返回给浏览器的结果却被拦截,被识别为一次失败的请求,这时候可能对数据库里数据已经产生了影响。为了防止这种情况发生,这种可能对服务器数据产生操作的HTTP请求,浏览器必须先试用OPTIONS 方法发起预检请求,从而获知服务器是否允许该跨域请求。

什么情况下会视为OPTIONS 预检请求通过?

带着疑问继续查看相关博客,看到很多人都是通过拦截OPTIONS请求,并设置相应的返回码来处理预检请求。

location /crosstest/web/ {
    add_header Access-Control-Allow-Origin: * ;
    add_header Access-Control-Allow-Credentials true;
    add_header Access-Control-Allow-Methods GET,PUT,POST,DELETE,OPTIONS;
    add_header Access-Control-Allow-Headers Content-Type,* ;
    proxy_pass http://webserver/web;
    if ($request_method = "OPTIONS"){
        return 200;
    }
}

经过测试,确实可行。

nginx 拦截post请求 nginx拦截get请求_nginx 拦截post请求_02

总结一下预检请求通过的条件

  1. OPTIONS 方法获取的Response header 字段中,有Access-Control-* 的字段。表示服务器告知浏览器服务端支持跨域访问的方法和header 字段等。
  2. 复杂请求中的request method、request header、Origin等满足第1点中服务端告知给客户端的服务器端接受的条件。
  3. 当前两点都满足的时候,浏览器会才会发起跨域请求,否则浏览器直接拦截跨域请求,该请求不会走到服务器端。

其他补充
4. Access-Control-Max-Age 可以指定将预检请求的结果缓存多长时间,在这个时间范围内就不用再重复发起预检请求了
5. Access-Control-Allow-Credentials true 是否允许在跨域请求中传递cookie ,默认false
6. Access-Control-Allow-Origin 要根据实际允许跨域的origin填写,* 表示允许所有origin 跨域访问