详解 CORS

引子

今天在调试前端应用向本地后台发送请求的时候,chrome控制台报错:

Access to XMLHttpRequest at 'http://localhost:3051/xxxxxx' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

类似的错误,还有

Access to XMLHttpRequest at 'http://127.0.0.1:3051/dealerlist' from origin 'http://localhost:8080' has been blocked by CORS policy: Request header field timestamp is not allowed by Access-Control-Allow-Headers in preflight response.

错误提示告诉我们是由 CORS 策略引起的。
在本地同时开发调试前后端的过程中,经常会遇到这个问题--CORS 跨域资源共享。

CORS(Cross-Origin Resource Sharing)

现代浏览器有一个最基本的安全功能:同源策略(same-origin security policy)。简单的说,浏览器页面上的请求必须和当前页面是同源的。而所谓同源指的是: 域名、协议和端口相同。因此,在本地开发的时候,因为前后端的端口号不同,并且服务器没有做相应的处理,会发生上述错误。这种情况下,通过CORS定义了如何实现资源的跨域共享。标准中主要是通过在header中添加了一些字段来解决允许浏览器访问哪些资源。
根据mozilla的文档,下面介绍三种需要使用CORS的场景。

1. 简单请求

如果服务器正确了处理CORS协议,简单请求可以直接接收到数据。简单请求需要满足一定的条件,比如方法是HEAD、GET和POST之一,Content-Type值为text/plain,multipart/form-data和application/x-www-form-urlencoded之一等等。

CROS 简单请求

如上图所示,简单请求中header含有origin,服务器响应中header含有 Access-Control-Allow-Origin,通过这两个字段可以实现基本的资源控制。

2. 预检请求 preflight

这种场景指的是发送请求前,浏览器会先发送预检请求,如果接收到了服务器“正确”的响应,浏览器才会再次发送请求。因此,这种场景下,一次成功的CORS请求包含2次请求。

preflight request

上面的示例很好地描述了这一过程:

  1. 浏览器首先发送预检请求
  2. 服务器响应预检请求
  3. 浏览器发送实际的请求
  4. 服务器响应

在第一步中,请求使用 OPTIONS 方法,header须包含如下字段:

  • Access-Control-Request-Method: 告诉后台,(如果后台允许CORS请求)随后发起的CORS请求的方法
  • Access-Control-Request-Headers:和上面类似,告诉后台随后发起的CORS请求中特有的的headers

服务器根据预检请求,结合自身的逻辑,做出响应,响应的header须包含:

  • Access-Control-Allow-Origin 说明是否允许CORS,值是一个字符串。可以指定特定的站点名,或者'*'表示不做限制。

  • Access-Control-Allow-Credentials 非必须,主要用于第三个场景。当浏览器的credentials设置为true时是否允许浏览器读取response的内容。当用在对preflight预检测请求的响应中时,它指定了实际的请求是否可以使用credentials。

  • Access-Control-Allow-Methods 服务器允许的请求方法;如果有多个,用','分割

  • Access-Control-Allow-Headers 服务器允许的请求headers头部;如果有多个,用','分割。用于header中的自定义字段必须在这里完整声明。

  • Access-Control-Max-Age Access-Control-Allow-MethodsAccess-Control-Allow-Headers 的缓存时间,单位为秒。在该时间段内,浏览器可以直接发送CORS请求而不需要先发送preflight请求。

  • Access-Control-Expose-Headers 注意该header仅适用于非preflight请求。非同源请求的响应通常只能访问基本的的响应,如Content-Language、Content-Type,如果需要访问其他header内容,可以在这里添加;如果有多个,用','分割

根据第二步的响应,浏览器构造“正确”的请求,这样就能得到服务器的正确响应。

3. 带身份凭证的请求

这种场景很有趣,它允许通过cookies和credential信息来发送请求。

CORS with Credentials
如图所示,浏览器发起的请求中,带有cookies,服务器的响应中header带有 Access-Control-Allow-Credentials。如果没有该字段,浏览器不会将正确的结果返回。而正因为请求带有cookie,服务器响应中Access-Control-Allow-Origin不能是通配符,而必须是请求中的 origin 值。

实践

下面以第二种场景为例,进行简单的确认。
前端请求使用axios;后台服务器使用Express搭建,端口3051,创建一个/list服务,需:

  • 使用 POST
  • Header中含有自定义的 timestamp

前端发起请求部分:

let axiosConfig = {
    method: 'post',
    url: 'http://127.0.0.1:3051/list',
    headers: {
      'timestamp':timestamp,
      'Content-Type': 'application/x-www-form-urlencoded',
      'uAgent': 'myWebApp'
    }
  };
  const self = this;
  axios(axiosConfig).then(function(res){
    console.log('res:', res);
  }).catch(function(error){
    console.log('error:', error);
  });

服务器部分:

//app.jsapp.all('*', function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Origin,X-Requested-With,timestamp,uAgent");
  res.header("Access-Control-Allow-Methods","POST,GET,OPTIONS");
  next();
});

当浏览器发送请求后,可以在服务器控制台中首先看到:

OPTIONS /list 200

这个预检请求是浏览器自行构造并发送的。上述的服务器 all 部分,添加了header参数,告诉前端该如何发起跨域请求。这里为了简单,直接对所有的API添加了CORS定义的header参数。实际中,可以根据请求的方法,origin 参数进行详细地控制。

另外,关于 user-agent参数,chrome不允许修改,所以这里定义了 uagent

总结

这里我们主要学习了:

  1. 为什么会发生CORS请求
  2. 如何遵循CORS协议,正确地发送请求和做出响应

参考资料

mozilla: Access control CORS
http cors protocol