CORS详解

CORS(Cross-Origin Resource Sharing): 跨域资源共享。是个W3C标准。

解决Ajax跨域通信的最根本的方法。

 

根据请求是否会触发浏览器的预检请求(OPTIONS),将CORS请求分为两类。

1. 简单请求

1. 定义

同时满足下面的所有条件:

1. 请求方法是GET、HEAD、POST

2.请求头不超出以下几种,可以是其中一部分:

1. Accept
2. Accept-Language
3. Content-Language
4. Content-Type:只包含application/x-www-form-urlencoded, multipart/form-data, text/plain三种

2.特征

1)对于简单请求,浏览器会自动添加一个Origin字段,表示发起请求的源(协议+域名+端口)。

服务器根据这个字段来判断,是否允许本次请求。

2)如果服务器端不做任何设置,跨域请求抛出错误,触发onerror事件。

该错误事件无法通过状态码识别,因为浏览器的响应码是200。

错误信息如下:
Access to XMLHttpRequest at 'http://localhost:3001/query' from origin 'http://localhost:3000' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

实际上请求成功发送到了服务端,但是浏览器拦截了响应

3)在跨域访问时,xhr.getAllResponseHeaders方法只能获取一些基本的响应头。

Content-Length
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma

如果服务器端想要通过响应头向客户端发送数据:

res.setHeader('X-Custom-Response', 'value')

但是由于同源策略限制,客户端读不到该响应头。

则需要在服务器端允许客户端读取该响应头:

res.setHeader('Access-Control-Expose-Headers', 'X-Custom-Response');

3. 简单请求解决跨域问题

1) 因为AJAX请求出现跨域限制的本质是浏览器拦截了服务器的响应。

CORS请求能够跨域的本质是在服务器响应中告诉浏览器,服务器允许跨域,就可以解决问题。

将下面的字段设置为请求头中的Origin字段对应的源:

res.setHeader('Access-Control-Allow-Origin', origin/*);
//服务器允许请求的源:请求头中的Origin或者*

2) CORS请求浏览器默认不自动携带Cookie(及Http认证信息),如果需要发送请求时携带Cookie

客户端需要设置:

xhr.withCredentials = true; //允许CORS携带Cookie,domain是当前源的cookie

设置后,请求头会自动添加一个Cookie字段,携带对应的Cookie信息。

如果只有客户端设置该请求头,服务器端没有对应设置,则浏览器会抛出错误:

Access to XMLHttpRequest at 'http://localhost:3001/query' from origin 'http://localhost:3000' has been blocked by CORS policy: 
The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.
The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

所以服务器端要允许该属性:

res.setHeader('Access-Control-Allow-Credentials', true);  //服务器端允许发送Cookie

此时

res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000'); //只能对应请求头中的Origin,不能是*;否则报错

4. 应用

客户端(3000)示例:

const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
  if (this.readyState === 4) {// 结束,成功或者失败
    if (this.status >=200 && this.status <=300 || this.status ===304) {
      console.log(this.response); //end
      console.log(this.getAllResponseHeaders()); 
      /*
      content-length: 3  //默认只返回这个
      x-custom-response: value // 服务器端暴露的响应头
      */
    }
  }
}
xhr.onerror = function(e) {
  console.log('error')
}
xhr.open('GET', 'http://localhost:3001/query',true);
xhr.withCredentials = true;
xhr.send();

服务器端(3001)示例:

const express = require('express');
const app = express();

// 服务器的请求白名单
const whiteLists = ['http://localhost:3000'];
app.use(function(req, res, next) {
    const { origin } = req.headers; // 跨域也可以获取到请求头
    if (whiteLists.includes(origin)) {
        res.setHeader('Access-Control-Allow-Origin', origin);
        res.setHeader('Access-Control-Allow-Credentials', true);
        res.setHeader('Access-Control-Expose-Headers', 'X-Custom-Response');
    }
    next();
})
app.get('/query', function(req,res) {
    res.setHeader('X-Custom-Response', 'value'); //通过请求头返回数据
    res.end('end')
})
app.listen(3001);

2. 非简单请求

1. 定义

不是简单请求的AJAX请求,下面只要有一个条件满足就是非简单请求:

1)PUT,DELETE请求

2) 自定义请求头

3)Content-Type: "application/json"

2. 特征

非简单请求,会在正式请求前发出一个“预检”请求(preflight)。

通过请求方法OPTIONS发起预检请求,先询问服务器,请求的源(Origin)/请求头/请求方法是否在服务器的白名单中。

如果全部是,浏览器再发起正式CORS请求。如果其中一个是否,则抛出错误。

3. OPTIONS预检

1)同简单请求,先询问Origin是否在白名单;

如果有withCredentials, 询问该属性是否在响应头中。需要设置:

res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', true);

2)如果CORS请求使用特殊请求方法,如DELETE或者PUT;

xhr.open('DELETE', url, true);

那么OPTIONS请求头除了会自动添加Origin字段外,OPTIONS请求还会自动添加:

Access-Control-Request-Method': 'DELETE'
// 或者(根据请求方法决定)
Access-Control-Request-Method': 'PUT'

此时如果服务器端不做设置,默认只允许GET/HEAD/POST。

因为DELETE/PUT不在默认允许范围内,浏览器抛出异常:

Access to XMLHttpRequest at 'http://localhost:3001/query' from origin 'http://localhost:3000' has been blocked by CORS policy: 
Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.

此时应该在服务器端返回对应的响应头,告诉浏览器,服务器端支持该请求方法:

res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');

3)如果使用自定义请求头向服务器发送数据,或者使用特殊的Content-Type:

xhr.setRequestHeader('X-Custom-Header', 'value')
// 或者
xhr.setRequestHeader('Content-Type', 'application/json')

OPTIONS请求头除了自动添加Origin,Access-Control-Request-Method外,还会自动添加

Access-Control-Request-Headers: 'content-type,x-custom-header'

如果服务器端不允许该请求头,那么在预请求阶段,浏览器抛出异常:

Access to XMLHttpRequest at 'http://localhost:3001/del' from origin 'http://localhost:3000' has been blocked by CORS policy: 
Request header field x-custom-header is not allowed by Access-Control-Allow-Headers in preflight response.

解决该问题,服务器端需要设置允许特殊请求头访问:

res.setHeader('Access-Control-Allow-Headers', 'Content-Type,X-Custom-Header');

4)如果需要缓存预检结果,设置:

Access-Control-Max-Age: 10000; //单位是s(秒)

设置后,在规定时间内,即使是非简单请求,也不再发起“预检”请求。

4. 预检成功

成功通过“预检”后,发起真实的CORS请求,此时请求头不再包含跨域的请求头信息。

Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Content-Type: application/json
Cookie: a=b
Host: localhost:3001
Origin: http://localhost:3000
Referer: http://localhost:3000/
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36
X-Custom-Header: value

5. 应用

客户端(3000端口)代码:

 const xhr = new XMLHttpRequest();
 xhr.onreadystatechange = function() {
   if (this.readyState === 4) {// 结束,成功或者失败
     if (this.status >=200 && this.status <=300 || this.status ===304) {
       console.log(this.response);
     }
   }
 }
 xhr.onerror = function(e) {
   console.log('error')
 }
 xhr.open('DELETE', 'http://localhost:3001/del',true);
// 预检成功后,会再次执行;不能在预检阶段res.end();否则下面的代码会导致服务器报错 xhr.withCredentials
= true; xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('X-Custom-Header', 'value') xhr.send();

服务器端(3001)代码: 

const express = require('express');
const app = express();

// 服务器的请求白名单
const whiteLists = ['http://localhost:3000'];
app.use(function(req, res, next) {
    const { origin } = req.headers; // 跨域也可以获取到请求头
    if (whiteLists.includes(origin)) {
        //允许访问的源
        res.setHeader('Access-Control-Allow-Origin', origin); 
        //允许携带Cookie
        res.setHeader('Access-Control-Allow-Credentials', true); 
        // 允许客户端读取的响应头
        res.setHeader('Access-Control-Expose-Headers', 'Connection, Date'); 
        // 如果请求方法是GET/HEAD/POST,该设置可以省略
        res.setHeader('Access-Control-Allow-Methods', 'GET,DELETE,PUT,POST');
       // 允许出现的请求头
        res.setHeader('Access-Control-Allow-Headers', 'Content-Type,X-Custom-Header');
        //发起一次后,10s再请求不再发起预检; 预检存留时间
        res.setHeader('Access-Control-Max-Age', 10); 
    }
    next();
})
app.delete('/del', function(req,res) {
    res.end('del end')
})
app.listen(3001);

 

posted @ 2019-11-14 16:08  Lyra李  阅读(805)  评论(0编辑  收藏  举报