js跨域

2019-07-26  本文已影响0人  Super曲江龙Kimi

js跨域

1.同源策略

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制内容有:
Cookie、LocalStorage、IndexedDB 等存储性内容
DOM 节点
AJAX 请求发送后,结果被浏览器拦截了

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

2.常见跨域场景

当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。常见跨域场景如下图所示:

1.png

3. 实现跨域的九种方式

jsonp

有几个标签是允许跨域加载资源:

<img src=XXX>
<link href=XXX>
<script src=XXX>
<frame>等dom标签,还有样式中background:url()、@font-face()等文件外链

JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击(jsonp请求返回<script></script>)。

   <script>
      function kimi(data) {
        console.log(data)
      }
    </script>
    <script src="https://www.baidu.com/sugrec?prod=pc&wd=8&cb=kimi">
    </script>

cors

浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。

服务端设置了Access-Control-Allow-Origin就开启了CORS通信,表示允许哪个域来访问我。
具体需要设置的头部有很多,列举常用的:

    // 设置哪个源可以访问我
    res.setHeader('Access-Control-Allow-Origin', origin)
    // 允许携带哪个头访问我
    res.setHeader('Access-Control-Allow-Headers', 'name')
    // 允许哪个方法访问我
    res.setHeader('Access-Control-Allow-Methods', 'PUT')
    // 允许携带cookie
    res.setHeader('Access-Control-Allow-Credentials', true)
    // 预检的存活时间
    res.setHeader('Access-Control-Max-Age', 6)
    // 允许返回的头
    res.setHeader('Access-Control-Expose-Headers', 'name')

postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

// a.html
  <iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe>
  
    <script>
      function load() {
        let frame = document.getElementById('frame')
        frame.contentWindow.postMessage('kimi', 'http://localhost:4000') //发送数据
        window.onmessage = function(e) { //接受返回数据
          console.log(e.data) //yes
        }
      }
    </script>
// b.html
  window.onmessage = function(e) {
    console.log(e.data) //kimi
    e.source.postMessage('yes, e.origin)
 }

http-proxy

一般在使用webpack时webpack-dev-server会加proxy-table来转发请求。因为同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略

nginx

实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。

//  nginx proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;
    }
}

websocket

websocket 是没有跨域限制的。

// socket.html
<script>
    let socket = new WebSocket('ws://localhost:3000');
    socket.onopen = function () {
      socket.send('kimi1');//向服务器发送数据
    }
    socket.onmessage = function (e) {
      console.log(e.data);//接收服务器返回的数据
    }
</script>
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
  ws.on('message', function (data) {
    console.log(data);
    ws.send('kimi2')
  });
})

document.domain

该方式只能用于二级域名相同的情况下,比如 video.baidu.comwww.baidu.com 适用于该方式。

// a.html
<body>
 helloa
  <iframe src="http://video.baidu.com" frameborder="0" onload="load()" id="frame"></iframe>
  <script>
    document.domain = 'baidu.cn'
    function load() {
      console.log(frame.contentWindow.a);
    }
  </script>
</body>
// b.html
<body>
   hellob
   <script>
     document.domain = 'baidu.cn'
     var a = 100;
   </script>
</body>

window.name

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

实现:先在a页面中嵌入c页面,在c页面中写入window.name值, c加载完毕后马上切换成a同源的b页面。等b加载完后可以拿到没有清除的name值

// a.html(http://localhost:3000/b.html)
  <iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
  <script>
    let first = true
    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    function load() {
      if(first){
      // 第1次onload(跨域页)成功后,切换到同域代理页面
        let iframe = document.getElementById('iframe');
        iframe.src = 'http://localhost:3000/b.html';
        first = false;
      }else{
      // 第2次onload(同域b.html页)成功后,读取同域window.name中数据
        console.log(iframe.contentWindow.name);
      }
    }
  </script>
 // c.html(http://localhost:4000/c.html)
  <script>
    window.name = 'yes'  
  </script>

location.hash

a页面嵌入c页面带给他hash数据,c页面中拿到后,再嵌入和a同源的b页面带入hash数据,b拿到后再设置回a页面的hash中。a中再监听hash变化来获取。

// a.html
  <iframe src="http://localhost:4000/c.html#kimi"></iframe>
  <script>
    window.onhashchange = function () { //检测hash的变化
      console.log(location.hash);
    }
  </script>
 // b.html
  <script>
    window.parent.parent.location.hash = location.hash 
    //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
  </script>
 // c.html
 console.log(location.hash);
  let iframe = document.createElement('iframe');
  iframe.src = 'http://localhost:3000/b.html#yes';
  document.body.appendChild(iframe);

总结

现在业界主流使用解决跨域问题的方案主要是 nginx和cors、jsonp(优势在于可以兼容老版本浏览器,但是只支持get并且不安全),本地开发常用node-proxy的方式做反向代理。其他的方式都不常用。location.hash和window.name都需要有第三方中介。而domain的方式会有域名的限制条件。

上一篇 下一篇

猜你喜欢

热点阅读