前端总结(二)跨域

参考:https://blog.csdn.net/frontender_way/article/details/70568113 

一、同源策略:

同源策略就是浏览器为了保证用户信息的安全,防止恶意的网站窃取数据,禁止不同域之间的JS进行交互。

对于浏览器而言只要域名、协议、端口其中一个不同就会引发同源策略,从而限制他们之间如下的交互行为:

(1)Cookie、LocalStorage 和 IndexDB 无法读取。
(2)DOM 无法获得。
(3)AJAX 请求不能发送。

二、什么是跨域?

协议、主机地址(或域名)、端口其中有一项不同,就认为是不同域,这之间发生任何数据交互都是跨域。

三、什么是JS跨域?

两个不同的域a、b

在a的应用的js脚本中调用了b的后端地址。 

js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同域的框架中(iframe)的数据。

只要协议、域名、端口有任何一个不同,都被当作是不同的域。

四、有哪些跨域方法?

(1)JSONP

问题: 假设 a 网站请求 b 网站的一个 js,这个 js 中请求了 b 网站的内容算跨域吗。

在js中,我们直接用XMLHttpRequest请求不同域上的数据时,是不可以的。

但是,在页面上引入不同域上的js脚本文件却是可以的,script标签里的src属性来完成的,jsonp正是利用这个特性来实现的。

比如,新建一个crossDomain.html页面,它里面的代码需要利用ajax获取一个不同域上的json数据,假设这个json数据地址是http://192.168.x.xxx/JSONP/jsonpTest.php那么crossDomain.html中的代码就可以这样:

<script type="text/javascript">
var text = document.querySelector('.text');
function dosomething(jsondata) {
    var str = "";
    for (var i = 0; i < jsondata.length; i++) {
        str += jsondata[i];
    }
    text.innerHTML = '我是JS通过JSONP跨域请求来的数据:'+'<span class="show">'+str+'</span>';
}
</script>
<script type="text/javascript" src="http://192.168.x.xxx/JSONP/jsonpTest.php?callback=dosomething"></script>

可以看到在获取数据的地址后面还有一个callback参数,按惯例是用这个参数名,但是你用其他的也一样。当然如果获取数据的jsonp地址页面不是你自己能控制的,就得按照提供数据的那一方的规定格式来操作了。

因为是当做一个js文件来引入的,所以http://192.168.x.xxx/JSONP/jsonpTest.php返回的必须是一个能执行的js文件,所以这个页面的php代码可能是这样的:

<?php
    $callback = $_GET['callback'];//得到回掉函数名
    $data = array('a','b','c'); //要返回的数据
    echo $callback.'('.json_encode($data).')'; //输出
?>

然后在crossDomain.html中打印出返回的jsondata如下:

["a", "b", "c"]

可以看到请求成功了,然后就可以在crossDomain.html这个页面里处理这个数据了。
这样jsonp的原理就很清楚了,通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的函数,并且会把我们需要的json数据作为参数传入。所以jsonp是需要服务器端的页面进行相应的配合的。
当然可以直接用一些已经封装过的库,这样就不用每次去创建script标签了。如下为JQ的跨域API:

$.getJSON('http://192.168.x.xxx/JSONP/jsonpTest.php?callback=?',function(jsondata){
        console.log(jsondata);//["a", "b", "c"]
        var str = "";
        $.each(jsondata,function(i,index){
            return str += index;
        });
        $(".text1").html('我是JQ通过JSONP跨域请求来的数据:'+'<span class="show">'+str+'</span>');
    });

jquery的getJSON方法会自动生成一个全局函数来替换callback=?中的问号,之后获取到数据后又会自动销毁,实际上就是起一个临时代理函数的作用。$.getJSON方法会自动判断是否跨域,不跨域的话,就调用普通的ajax方法;跨域的话,则会以异步加载js文件的形式来调用jsonp的回调函数。

var script = document.createElement('script');
  script.type = 'text/javascript';

  // 传参并指定回调执行函数为onBack
  script.src = 'http://www.domain-com:8080/login?user=admin&callback=onBack';
  document.head.appendChild(script);

  // 回调执行函数
  function onBack(res) {
      alert(JSON.stringify(res));
  }

 

(2)通过修改document.domain来跨子域

上面的jsonp是来解决ajax跨域请求的,那么如果是需要处理 Cookie 和 iframe 该怎么办呢?这时候就可以通过修改document.domain来跨子域。两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain共享 Cookie或者处理iframe。比如A网页是http://w1.example.com/a.html,B网页是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享Cookie。

document.domain = 'example.com';
//现在,A网页通过脚本设置一个 Cookie。
document.cookie = "test1=hello";
//B网页就可以读到这个 Cookie。
var allCookie = document.cookie;

注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。
另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如.example.com。

Set-Cookie: key=value; domain=.example.com; path=/
//这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。

不同的iframe 之间(父子或同辈),是能够获取到彼此的window对象的,但是你却不能使用获取到的window对象的属性和方法(html5中的postMessage方法是一个例外,还有些浏览器比如ie6也可以使用top、parent等少数几个属性),总之,你可以当做是只能获取到一个几乎无用的window对象。
首先说明一下同域之间的iframe是可以操作的。比如http://127.0.0.1/JSONP/a.html里面嵌入一个iframe指向http://127.0.0.1/myPHP/b.html。那么在a.html里面是可以操作iframe里面的DOM的。

<iframe src="http://127.0.0.1/myPHP/b.html" frameborder="1"></iframe>
<body>
<script type="text/javascript">
var iframe = document.querySelector("iframe");
iframe.onload = function(){
    var win = iframe.contentWindow;
    var doc = win.document;
    var ele = doc.querySelector(".text1");
    var text = ele.innerHTML="123456";
}
</script>

如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。如果两个窗口一级域名相同,只是二级域名不同,那么document.domain属性,就可以规避同源政策,拿到DOM。 


对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题。

  1. 片段识别符(fragment identifier)
  2. window.name
  3. 跨文档通信API(Cross-document messaging)

(3)使用片段识别符来进行跨域

片段标识符(fragment identifier)指的是,URL的#号后面的部分,比如http://example.com/x.html#fragment的#fragment。如果只是改变片段标识符,页面不会重新刷新。 
父窗口可以把信息,写入子窗口的片段标识符。在父窗口写入:

document.getElementById('frame').onload = function(){
    var src = "http://127.0.0.1/JSONP/b.html" + '#' + "data";
    this.src = src;
}

子窗口通过监听hashchange事件得到通知。

window.onload = function(){
    console.log("b.html加载完成")
    window.onhashchange = function(){
        var message = window.location.hash;
        console.log(message)//#data
    };  
}

同样的,子窗口也可以改变父窗口的片段标识符。

parent.location.href= target + "#" + hash;

(4)使用window.name来进行跨域

window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。
比如:有一个页面a.html,它里面有这样的代码:

window.name = "我是a页面设置的";
setTimeout(function(){
    window.location = "http://127.0.0.1/JSONP/b.html";
},1000)

b.html页面的代码:

console.log(window.name);

a.html页面载入后1秒,跳转到了b.html页面,结果b页面打印出了:

我是a页面设置的

可以看到在b.html页面上成功获取到了它的上一个页面a.html给window.name设置的值。如果在之后所有载入的页面都没对window.name进行修改的话,那么所有这些页面获取到的window.name的值都是a.html页面设置的那个值。当然,如果有需要,其中的任何一个页面都可以对window.name的值进行修改。注意,window.name的值只能是字符串的形式,这个字符串的大小最大能允许2M左右甚至更大的一个容量,具体取决于不同的浏览器,但一般是够用了。

利用window.name可以对同域或者不同域的之间的js进行交互。

那么在a.html页面中,我们怎么把b.html页面载入进来呢?显然我们不能直接在a.html页面中通过改变window.location来载入b.html页面,因为我们想要即使a.html页面不跳转也能得到b.html里的数据。答案就是在a.html页面中使用一个隐藏的iframe来充当一个中间人角色,由iframe去获取b.html的数据,然后a.html再去得到iframe获取到的数据。
(5)window.postMessage

上面两种方法都属于破解,HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。 

这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。

举例来说,父窗口http://a.com向子窗口http://b.com发消息,调用postMessage方法就可以了。

a页面:

<iframe id="frame1" src="http://127.0.0.1/JSONP/b.html" frameborder="1"></iframe>
document.getElementById('frame1').onload = function(){
    var win = document.getElementById('frame1').contentWindow;
    win.postMessage("我是来自a页面的","http://127.0.0.1/JSONP/b.html")
}

b页面通过监听message事件可以接受到来自a页面的消息。

window.onmessage = function(e){ 
  e = e || event;
  console.log(e.data);//我是来自a页面的
}

子窗口向父窗口发送消息的写法类似。

window.opener.postMessage('我是来自b页面的', 'http://a.com');
//父窗口和子窗口都可以通过message事件,监听对方的消息。

通过window.postMessage,读写其他窗口的 LocalStorage 也成为了可能。 
下面是一个例子,主窗口写入iframe子窗口的localStorage。 
父窗口发送消息代码

var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 存入对象
win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://b.com');
// 读取对象
win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");
window.onmessage = function(e) {
  if (e.origin != 'http://a.com') return;
  // "Jack"
  console.log(JSON.parse(e.data).name);
};

子窗口接收消息的代码

window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') return;
  var payload = JSON.parse(e.data);
  switch (payload.method) {
    case 'set':
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
      break;
    case 'get':
      var parent = window.parent;
      var data = localStorage.getItem(payload.key);
      parent.postMessage(data, 'http://aaa.com');
      break;
    case 'remove':
      localStorage.removeItem(payload.key);
      break;
  }
};

(6)通过WebSocket进行跨域

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。 

下面是一个例子,浏览器发出的WebSocket请求的头信息

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

上面代码中,有一个字段是Origin,表示该请求的请求源(origin),即发自哪个域名。 
正是因为有了Origin这个字段,所以WebSocket才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

(7)CORS

CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

(8)服务端设置代理页面专门处理前端跨域请求

 

总结:以上整理了各种常见的跨域解决办法,在开发过程中我们可以根据不同的场景选择最佳的解决办法。处理ajax的跨域可以选择JSONP、CORS,服务端设置代理、WebSocket。如果主域相同,处理多级子域之间的通信可以选择document.domain,处理不同域之间的iframe,子窗口可以选择window.name、window.postMessage、location.hash来解决。

五、怎么发送一个跨域的POST请求?

六、跨域方式

 

  • jsonp
      var script = document.createElement('script');
      script.type = 'text/javascript';
    
      // 传参并指定回调执行函数为onBack
      script.src = 'http://www.domain-com:8080/login?user=admin&callback=onBack';
      document.head.appendChild(script);
    
      // 回调执行函数
      function onBack(res) {
          alert(JSON.stringify(res));
      }
    
  • document.domain + iframe跨域
      //父窗口:(http://www.domain.com/a.html)
      <iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
      <script>
          document.domain = 'domain.com';
          var user = 'admin';
      </script>
      //子窗口:(http://child.domain.com/b.html)
      <script>
          document.domain = 'domain.com';
          // 获取父窗口中变量
          alert('get js data from parent ---> ' + window.parent.user); //"admin"
      </script>
    
  • location.hash + iframe

     

    与document.domain类似,不同的是,通过修改父页面的iframe的src进而达到修改window.hash的效果,子页面通过window.onhashchange来监听

  • window.name + iframe跨域
  • 跨域资源共享(CORS)

     

    前端正常请求,后端设置:

      res.writeHead(200, {
          'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
          'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
          /* 
              * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
              * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
              */
          'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
      });
    
  • nginx代理跨域
  • nodejs中间件代理跨域
  • WebSocket协议跨域

 




 

posted @ 2019-02-22 15:40  小杜村  阅读(205)  评论(0编辑  收藏  举报