CORS学习总结

545 阅读4分钟

跨域产生原因:

浏览器同源策略。限制不同源的脚本相互访问。假设没有同源策略,将一个网站以iframe的形式嵌入到我的一个网站中,然后我就能直接访问到那个网站的用户名密码,就会产生严重的安全隐患。但是这个策略其实是浏览器实现的,请求还是能发送,但是返回的内容被浏览器截取了。

跨域的解决方法

1. CORS

这个也是比较推荐的做法。通过在response.header中设置**Access-Control-Allow-Origin: [hostname]**决定允许哪个origin请求数据。hostname可以是一个具体的地址,也可以是 * 这种通配符,这就意味着允许所有不同源的地址请求数据,一般生产环境最好不要这样设置。

如果想实现允许多个地址又不是所有地址都访问,可以先判断这些请求origin是否在允许范围内,再决定是否设置Access-Control-Allow-Origin为某个地址。

浏览器针对CORS请求分为 简单请求、非简单请求。

简单请求:

  1. 请求方法为:get, post, head
  2. http头信息仅为:Accept, Accept-Langage, Content-Language, Last-Event-ID, Content-Type仅为application/x-www-form-urlencoded、multipart/form-data、text/plain

所有不是简单请求都为非简单请求。

如果是简单请求:

请求发起时,request.header中会添加一个**origin(协议+域名+端口)**字段,服务器根据这个字段值来决定是否允许请求数据。即是否符合Access-Control-Allow-Origin的值,如果符合,会在response.header中添加Access-Control-Allow-Origin字段,浏览器看到返回头中有该字段,就允许服务器的返回值通过。这样xhr.responseText就能拿到相应值了。

客户端

(function(){
  //方法一:get请求发生的跨域,通过利用h5的方法,给返回的请求头加上Access-Control-Allow-Origin解决
  var url = 'http://127.0.0.1:3000/testCors';
  function getData(url){
    return new Promise(function(resolve, reject){
      var xhr = new XMLHttpRequest();
      xhr.open('get', url, true);
      xhr.onreadystatechange = function(){
        if(xhr.readyState == 4){
          if(xhr.status == 200 || xhr.status == 304){
            if(resolve){
              resolve(JSON.parse(xhr.responseText));
            }else{
              reject(error);
            }
          }
        }
      }
      xhr.send(null);
    });
  }
  getData(url).then(function(value){
    console.log('cors:')
    console.log(value);//{"type": "cors"}
  });
})();

服务端

router.get('/testCors', function(req, res, next) {
  /*
  ** 方法一,IE10+以上支持,*代表允许任何地址跨域访问,
  ** 可以设置为一个特定的地址,这样就只允许这个地址跨域访问
  */
  res.header("Access-Control-Allow-Origin", "*");
  res.send({"type": "cors"});
});
如果是非简单请求:

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

非简单请求发送的时候,会先发起一个预检请求(preflight)

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

预检请求的方法为options,在这次请求中,会检查一些request.header的信息是否符合服务器的限制条件,如果一切符合,再发起真正的请求。以后再发起该请求时,就不会在发起预检请求了,就可以直接发送了。

2. postMessage

postMessage主要解决客户端端跨域通信。比如不同源的iframe间的通信。

假设有A、B两个不同源的iframe,如果 A 想向 B 发送一个消息。

A iframe

var iframeA = document.querySelector('#iframeA').contentWindow;
iframeA.postMessage('hello iframeB, i am iframeA', domain);

B iframe

window.addEventListener('message', function(e){
  if(e.origin == iframeA.contentWindow.location.href){
    console.log(e.data);
  }
}, false)

语法:

window.postMessage(msg, targetOrigin, [transfer])

  1. window: 窗口的引用;
  2. msg: 需要传递的数据(会被结构化克隆算法序列化,无需自己序列化);
  3. targetOrigin: 指定哪些窗口能接收到消息,可以是字符串 '*' ,表示无限制。
  4. transfer 可选 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

window.addEventListener('message', (event) => {}, false);

  1. 监听事件类型为message;
  2. 事件对象event,有三个属性址:
  • data 传递过来的数据
  • origin 发送窗口的origin
  • source 发送窗口的引用,可以用于建立双向通信

最佳实践:我们在任何时候都应该先判断origin,再决定是否接受数据,同时,postMessage的时候最好也指明 targetOrigin.

3. JSONP

原理:

利用script标签不受同源策略的特性实现跨域

具体实现方式:

创建一个script标签,在script.src的后面添加一个参数(一般命名为callback),参数值为本地定一个的一个函数名。

服务端解析url,提取出callback参数值(即为方法名),将需要返回的数据作为参数传递给这个方法。这样,客户端的函数里面就可以拿到这个值,从而实现跨域请求。

客户端

function jsonpCallBack(result){
  console.log('jsonp:');
  console.log(result);
}

(function(){
  //方法二:jsonp
  var url = 'http://127.0.0.1:3000/testJsonp';
  var jsonpScript = document.createElement('script');
  jsonpScript.type = 'text/javascript';
  jsonpScript.src = url + '?callback=jsonpCallBack';
  document.head.appendChild(jsonpScript);
})();

服务端

router.get('/testJsonp', function(req, res, next){
  /*
  ** 方法二:jsonp
  */
  var data = JSON.stringify({"type": "jsonp"});
  var parms = urlLib.parse(req.url,true);
  var str = parms.query.callback + '(' + data +')';
  console.log(str);
  res.write(str);
  res.end();
})

4.代理服务器

还有一些例如document.domain、window.name等等就不在这里介绍了。重点推荐使用 第一第二种方法。