添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

二、两种请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。

(1) 请求方法是以下三种方法之一:

(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值 application/x-www-form-urlencoded multipart/form-data text/plain
  • 凡是不同时满足上面两个条件,就属于非简单请求。

    浏览器对这两种请求的处理,是不一样的。

    三、简单请求

    3.1 基本流程

    对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个 Origin 字段。

    下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个 Origin 字段。

    GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...

    上面的头信息中, Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

    如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin 字段(详见下文),就知道出错了,从而抛出一个错误,被 XMLHttpRequest onerror 回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

    如果 Origin 指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

    Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8

    上面的头信息之中,有三个与CORS请求相关的字段,都以 Access-Control- 开头。

    (1)Access-Control-Allow-Origin

    该字段是必须的。它的值要么是请求时 Origin 字段的值,要么是一个 * ,表示接受任意域名的请求。

    (2)Access-Control-Allow-Credentials

    该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为 true ,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为 true ,如果服务器不要浏览器发送Cookie,删除该字段即可。

    (3)Access-Control-Expose-Headers

    该字段可选。CORS请求时, XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到6个基本字段: Cache-Control Content-Language Content-Type Expires Last-Modified Pragma 。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。上面的例子指定, getResponseHeader('FooBar') 可以返回 FooBar 字段的值。

    3.2 withCredentials 属性

    上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定 Access-Control-Allow-Credentials 字段。

    Access-Control-Allow-Credentials: true

    另一方面,开发者必须在AJAX请求中打开 withCredentials 属性。

    var xhr = new XMLHttpRequest(); xhr.withCredentials = true;

    否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

    但是,如果省略 withCredentials 设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭 withCredentials

    xhr.withCredentials = false;

    需要注意的是,如果要发送Cookie, Access-Control-Allow-Origin 就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的 document.cookie 也无法读取服务器域名下的Cookie。

    四、非简单请求

    4.1 预检请求

    非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUT DELETE ,或者 Content-Type 字段的类型是 application/json

    非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

    浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。

    下面是一段浏览器的JavaScript脚本。

    var url = 'http://api.alice.com/cors'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'value'); xhr.send();

    上面代码中,HTTP请求的方法是 PUT ,并且发送一个自定义头信息 X-Custom-Header

    浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。

    OPTIONS /cors HTTP/1.1 Origin: http://api.bob.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...

    "预检"请求用的请求方法是 OPTIONS ,表示这个请求是用来询问的。头信息里面,关键字段是 Origin ,表示请求来自哪个源。

    除了 Origin 字段,"预检"请求的头信息包括两个特殊字段。

    (1)Access-Control-Request-Method

    该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是 PUT

    (2)Access-Control-Request-Headers

    该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是 X-Custom-Header

    4.2 预检请求的回应

    服务器收到"预检"请求以后,检查了 Origin Access-Control-Request-Method Access-Control-Request-Headers 字段以后,确认允许跨源请求,就可以做出回应。

    HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain

    上面的HTTP回应中,关键的是 Access-Control-Allow-Origin 字段,表示 http://api.bob.com 可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

    Access-Control-Allow-Origin: *

    如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被 XMLHttpRequest 对象的 onerror 回调函数捕获。控制台会打印出如下的报错信息。

    XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

    服务器回应的其他CORS相关字段如下。

    Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000

    (1)Access-Control-Allow-Methods

    该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

    (2)Access-Control-Allow-Headers

    如果浏览器请求包括 Access-Control-Request-Headers 字段,则 Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。

    (3)Access-Control-Allow-Credentials

    该字段与简单请求时的含义相同。

    (4)Access-Control-Max-Age

    该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

    4.3 浏览器的正常请求和回应

    一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。

    下面是"预检"请求之后,浏览器的正常CORS请求。

    PUT /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com X-Custom-Header: value Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...

    上面头信息的 Origin 字段是浏览器自动添加的。

    下面是服务器正常的回应。

    Access-Control-Allow-Origin: http://api.bob.com Content-Type: text/html; charset=utf-8

    上面头信息中, Access-Control-Allow-Origin 字段是每次回应都必定包含的。

    五、与JSONP的比较

    CORS与JSONP的使用目的相同,但是比JSONP更强大。

    JSONP只支持 GET 请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

    文中提到“CORS请求默认不发送Cookie”,通过服务端响应 Access-Control-Allow-Credentials: true 来控制。但如果接口依赖Cookie来完成一些处理(比如登录态),那第一次发送CORS请求“默认不发送Cookie”的话,服务端接受不到Cookie,岂不会有问题?

    从实验上来看,Access-Control-Allow-Credentials: true 好像并没有控制Cookie是否发送,因为即使之前没有任何CORS请求(自然也没有往客户端发送该Header),第一次CORS请求也会带上所有该域能访问的Cookie。

    测试环境:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36

    我试了一下,如果设为 withCredentials = false ,就不会发送 Cookie 了。如果省略这一行,确实会发送。但 MDN 明确说,默认值是 false。https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials

    我原来的说法表达得不确切,我改一下。

    Access-Control-Allow-Origin 字段,如何表示允许一个以上的网站访问(不是*这种全部),比如 允许 http://api.bob.com、 http://api.alice.com 两个网站。

    Access-Control-Allow-Origin 字段能实现吗?我这边试下,好像不行。

    type: "POST",
    url: "http://192.168.0.103:8081/forum/servlet/HelloServlet",
    //contentType:"application/json",
    data: {uName:$("#uName").val(), uPwd:$("#uPwd").val()},
    dataType: "json",
    success: function(data){
    $('#resText').empty(); // 清空resText里面的所有内容
    var html = '';
    $.each(data, function(commentIndex, comment){
    html += '<div class="comment"><h6>' + comment['username']
    + ':</h6>

    + '</p></div>';
    $('#resText').html(html);

    服务端接收到后增加CORS处理。
    response.setContentType("text/html; charset=utf-8");
    response.addHeader("Access-Control-Allow-Origin", "*");
    response.addHeader("Access-Control-Allow-Methods", "*");
    response.addHeader("Access-Control-Max-Age", "100");
    response.addHeader("Access-Control-Allow-Headers", "X-Custom-Header,accept, content-type");
    response.addHeader("Access-Control-Allow-Credentials", "false");

    ===========================================
    为什么留言不能超过1200个字,接上一个提问
    ===========================================

    不加 `contentType="application/json"`正常访问后台。增加`contentType="application/json"`后提示:
    XMLHttpRequest cannot load http://192.168.0.103:8081/forum/servlet/HelloServlet. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1:8020' is therefore not allowed access.
    从错误看,应该是preflight (预检)请求涉及到跨域问题了。现在问题是preflight 的请求响应设置在哪边处理呢?

    Access-Control-Max-Age字段在webkit的浏览器上默认最大时间为5分钟,若设置超过5分钟则不会生效,代码见https://cs.chromium.org/chromium/src/third_party/WebKit/Source/core/loader/CrossOriginPreflightResultCache.cpp?rcl=1399481969&l=44
    另外,chrome实际上有bug,即使Access-Control-Max-Age设置为600,下次请求时依然会发送options请求,此bug在2012年被提出,但至今未修复。具体见 https://bugs.chromium.org/p/chromium/issues/detail?id=131368

    “上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。”

    对于简单请求,其并没有预检请求。也就是说收到服务器响应的时候,简单请求已经发出去了,也就是说简单请求发送的时候不管是带不带cookie,这个决策已经完成了。那么这时候server返回Access-Control-Allow-Credentials字段的意义是什么呢?

    1. 告诉浏览器其实服务器并没有处理你发送过来的cookie,假设浏览器发了cookie并期待cookie生效以达成某种目的的话,用来帮助定位问题?
    2. 告诉浏览器这个信息,下次再发同样的跨域请求的时候就知道该不该发送cookie了?

    最近要把web端已有的应用扩展到移动端h5轻应用上,遇到了跨域访问一个诡异的问题。在服务端设置好Access-Control-Allow-Origin:* ;Access-Control-Allow-Credentials: "true" 后,两个系统做了cas,然后开发环境一切ok。等到项目准备上线时,打包进weblogic,居然发现访问失效。具体表现是当请求的结果超出一定数据量以后(因为可以指定每页条数,所以可以一点点的加返回数据量,当到了一个临界值以后,突然出现问题,比如30条的时候没事,31条突然就没数据了),从浏览器观察返回的response为空。在weblogic开了debug模式,发现后端一切正常没有任何错误,也确实查了数据库也确实返回了。后来为了验证这个问题,把同一个包又放到tomcat上跑,发现能返回数据量的临界值更小了,同一个包放在本地jetty上就没这个问题。所以我怀疑是不是各个应用服务器在处理跨域返回数据量时有一个默认值,可以配置的?我找了文档却没发现有这个设置。百思不得其解,卡了我好几个周了,求大神解惑!!!
    今天我又做了几个实验,这个限额大约是20k的样子!!

    我就遇到了这种问题,angular 拦截器设置了 req.headers['token'] = ‘’; 跨域就无法通过预检测器,请问有什么方法么,我也设置了
    res.setHeader('Access-Control-Allow-Headers','token');
    但还是无法实现

    1)应该是那些应用有一个后台服务器,后台服务器调用的API
    2)restful api 是 web api 中的一种吧
    cors 只要浏览器 xhr 跨域请求都要面对的问题,和什么形式 api 无关
    web service 是很早之前的技术,现在应该是不流行了
    3)JSONP

    感谢,介绍的很详细。
    另外请问,如果返回头里包含两个Access-Control-Allow-Origin符合规范吗?比如返回头同时包含如下两个:
    Access-Control-Allow-Origin:www.origin.com //(请求发出域名)
    Access-Control-Allow-Origin:*

    在实际中遇上过上述问题,后台代码和IIS分别为返回头添加了Access-Control-Allow-Origin。当请求发出域名和接收域名(后台)是同一域名时可正常得到结果,但不是统一域名时就会报跨域错误。
    阮老师能帮忙解释一下吗?谢谢

    看文章的说法,浏览器决定是否跨域,那么服务端呢?简单的跨域,浏览器带着cookie请求服务器,服务器设置false,则不接收cookie?事实上cookie已经加载到request里了。
    第二个问题,简单的跨域中,如果服务端部允许跨域,那么服务端是直接返回空还是按照正常的逻辑返回了数据,只是到浏览器的时候被浏览器给拦截了?

    Response Headers
    Allow:GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
    Content-Length:0
    Date:Fri, 31 Mar 2017 06:12:56 GMT
    Request Headers
    Accept:*/*
    Accept-Encoding:gzip, deflate, sdch
    Accept-Language:zh-CN,zh;q=0.8
    Access-Control-Request-Headers:authorization
    Access-Control-Request-Method:POST
    Connection:keep-alive
    Host:10.220.17.121:8080
    Origin:http://10.220.18.113:8020
    Referer:http://10.220.18.113:8020/xb/wx/201703013/login.html
    User-Agent:Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36
    如何让浏览器通过Origin呢 怎么设置?

    一个post跨域请求在“预检”请求OPTIONS中Origin为什么会为null,并且会报错:

    Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access. The response had HTTP status code 401.

    OPTIONS请求http头部信息如下:

    Accept:*/*
    Accept-Encoding:gzip, deflate, sdch, br
    Accept-Language:zh-CN,zh;q=0.8
    Access-Control-Request-Headers:authorization
    Access-Control-Request-Method:POST
    Connection:keep-alive
    Host:chinapopin.com
    Origin:null
    User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36

    这真是绝世大坑!我搞了两天,查了无数资料,竟然有那么多网站说要把OPTIONSVerbHandler和WebDAV删除,事实上把OPTIONSVerbHandler删除了才会造成OPTIONS失败!
    难道是部分环境下会这样?

    总结一下我的解决方案(IIS10 + asp.net web api):
    1、IIS中增加响应头
    Access-Control-Allow-Origin:*
    Access-Control-Allow-Headers:Content-Type
    2、web.config中把“<remove name="OPTIONSVerbHandler" />”删掉

    仅此而已~

    Content-Language
    Last-Event-ID
    Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

    第一个简单请求的例子中就有
    Connection
    User-Agent
    这些都不再上面的字段中啊, 怎么理解阮大神说的 不能超过如下字段

    阮老师您好:
    我想咨询您个问题,当我进行跨域的post请求,这个请求为非简单请求的时候,我想要携带cookie,浏览器会做出一次预检请求,但是这次预检请求是并不携带cookie的,这就导致了服务端又新生成了一个cookie(然而这个cookie并用不上),请问一下是不是我哪里操作错误了还是其他什么情况~
    fetch("http://localhost:3000/login", {
    method: "post",
    mode: "cors",
    credentials: "include",
    headers: {'Content-Type': "application/json"}
    同理使用jq的ajax也一样
    $.ajax({
    url: 'http://localhost:3000/login',
    type: 'post',
    xhrFields: {
    withCredentials: true
    headers:{'Content-Type': "application/json"}
    并且jq请求回来的cookies在控制台里面看不到,当再次请求的时候却不知道缘何携带上了上次返回设置的cookie。。。但是fetch的请求可以在控制台看到返回的设置的cookie。。。
    不知道问题出在哪里,烦请老师解答,不胜感激~

    @阮一峰,如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。
    这里面有个错误吧,应该说服务器否定了“预检”请求,怎么变成浏览器了。

    阮老师,您好,看了您的这篇文章,感觉跨域请求的整个来龙去脉,算是基本明白了,

    但是还有一个问题,是不是 简单请求的情况下,就算服务器返回的响应头中,没有access-control-allow-origin的设置,但后端的服务并不会受影响,后端仍然会针对“简单请求”提供服务,只不过是浏览器自己比较高分亮节,不去处理返回的数据,

    是这样的吗,那跨域请求岂不是对于后端来说,仍然不是很安全, 除非在 比如Spring的filter中将跨域的请求进行过滤才行?

    阮老师你好,关于这个跨域我最近有个问题
    我电脑本机上用vue-cli的开发环境,去请求域名服务器的接口,我设置了
    Axios.defaults.withCredentials = true;
    后端是php代码
    if(isset($_SERVER["HTTP_ORIGIN"])) {
    header('Access-Control-Allow-Origin:'.$_SERVER["HTTP_ORIGIN"]);
    header('Access-Control-Allow-Methods:OPTIONS, GET, POST');
    header('Access-Control-Allow-Headers:x-requested-with');
    header('Access-Control-Max-Age:86400');
    header('Access-Control-Allow-Credentials:true');
    但是我在chrome network 中查看request header 还是没有发现携带cookie
    关于这方面,我看了其他的文章有说设置domain 但是document.domain 又被禁止,document.cookie = "domain=xxx.com"(xxx.com 是我请求的接口的网站域名)依旧不行
    我在这些设置中有些混乱,我始终没有把我本地的cookie 携带过去,后端要接受cookie 在登录状态判断。
    请教阮老师。我是否哪里遗漏了,还是做错了?还需要什么配置吗?谢谢阮老师

    阮老师,您好
    关于 Access-Control-Allow-Credentials 的有一部分不太理解
    有提到说 Access-Control-Allow-Credentials为true的时候才发送cookie
    但是这个时候不是已经接收到服务器的回应了吗? 这样的话第一次请求不是无法携带cookie吗?

    Spring Boot 一个 @CrossOrigin 就可以跨域访问了。 但是本地调试的时候,https://localhost:63343 的页面访问不通 https://localhost:8488的接口。

    有办法吗?感激不尽!

    "If request method is not a case-sensitive match for any method in methods and is not a simple method, apply the cache and network error steps."
    ----参见 https://www.w3.org/TR/cors/#preflight-request

    由于 GET | POST | HEAD 是简单方法,在通过预检请求后,即使 Access-Control-Allow-Methods 中没有返回 GET | POST | HEAD , 这三种类型的非简单请求仍然会成功。

    阮老师,简单请求的条件不只是您列的两大条件,根据mdn的文档,还有两个条件也需要同时满足:

    1. 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
    2. 请求中没有使用 ReadableStream 对象。

    https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

    依小仙来看,
    一个是至少貌似当Access-Control-Allow-Credentials:指定为 "true"时,Access-Control-Allow-Origin:不能指定为*,必须指定为具体的域名。
    二个是你后续出现的若干问题,是否与第一条有关?
    三个是服务器确实有返回,日志也看到了,哪就是客户端实际上也收到了,甚至返回200,但就是不给你,浏览器在cors时,只要发现服务器返回的headers有一点不匹配cors规则,就干这事,而且还不给你原因(还给你返回个200)

    这回复过去好久了,如果层主还在,不防说说原因

    1.我的情况是跨域问题时报、时不报。有时可以正常访问,有时又会报:has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource。
    2.nginx有代理,java代码也有写过滤器。网上查的试了不少,情况依旧。
    3.最早是有跨域问题,访问不到后端工程接口,加了过滤器(加 response.setHeader)后便可正常访问。
    4.最近又报此错,却也间或可正常访问。间或访问中突然又报错:502(Bad Gateway) ...

    个人甚为迷惑,求指点方向 ...

    nginx解决复杂请求跨域的时候需加上 aweys 参数 ,
    eg:add_header 'Access-Control-Allow-Origin' "$http_origin" always;
    原因:As of Nginx 1.7.5, add_header supports an "always" parameter which
    allows CORS to work if the backend returns 4xx or 5xx status code.
    参考资料:https://gist.github.com/Stanback/7145487