跨域解决方案

111 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第15天,点击查看活动详情

前言

本篇章主要讲述常见的跨域解决方案,跨域主要是因为浏览器同源策略引起的。

在之前的篇章中我们讲述了同源策略的产生背景,需要主要的是跨域是浏览器做出的限制,服务器与服务器通信并不存在跨域行为。

解决方案

CORS

在这篇 关于CORS的那点事 的文章里,我们介绍了什么是CORS以及如何设置等,我们需要知道的是CORS是目前处理跨域最流行的解决方案。

JSONP

JSONP的本质是通过script标签能够请求外部资源这一特性,让src属性链接外部资源实现跨域请求。这个跨域请求需要后端配合,让后端将数据以函数调用的形式返回,并告知浏览器以js文件执行。具体操作如下:

后端操作,以express服务器为例:

const data = [{a:1},{b:2},{c:3}]

(req,res,next) => {
    const json = JSON.stringify(data);
    const script = `callback(${json})`
    res.header("content-type","application/javascript")   //告知浏览器这是js文件,浏览器接收到会当js文件执行
    res.send(script)   //发送数据
}

假如前端不进行处理发送请求到后端的这个接口,浏览器控制台会报错,报错原因是callback这个函数不存在。所以,前端需要想要接收后端返回的数据,就需要定义一个函数接收这些数据。 前端操作:

function callback(data){
    //在这个函数里就可以对data进行操作了
}

所以,从上面的例子中我们也可以看出JSONP的缺点也是显而易见:

  • 影响服务器的响应格式。
  • 只能使用get请求,因为script标签只会发送get请求。

Node中间件代理

我们都知道服务器与服务器是不存在跨域的,所以我们可以通过一个服务器充当媒介进行跨域处理:

大致模式如下:

flowchart TB
    浏览器-->|请求|node服务器-->|请求| 目标服务器
    目标服务器-->|响应|node服务器-->|响应| 浏览器
    subgraph 浏览器
   
    end
    subgraph node服务器
   
    
    end
    subgraph 目标服务器
  
    end

Nginx反向代理

实现思路和Node服务器代理一样,需要搭建一个nginx服务器用于转发请求。通过 nginx 配置一个代理服务器(域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域登录。 大致模式如下:

flowchart TB
    浏览器-->|请求|服务器-->|请求| 目标服务器
    目标服务器-->|响应|服务器-->|响应| 浏览器
    subgraph 浏览器
   
    end
    subgraph 服务器
    nginx服务器
   
    
    end
    subgraph 目标服务器
  
    end


nginx配置大致如下:

// proxy服务器
server {
    listen   80;
    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;
    }
}

postMessage

postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一。

postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

关于用法MDN上有详细的文档说明:window.postMessage

http://localhost:3000/a.html

 <iframe src="http://localhost:6060/b.html" frameborder="0" id="frame" onload="load()"></iframe>//iframe加载完毕触发onload事件
    <script>
      function load() {
        const frame = document.getElementById('frame')
        frame.contentWindow.postMessage('hello', 'http://localhost:6060') //发送数据
        window.onmessage = (e) => { //接受返回数据
          console.log(e.data) //打印出hi
        }
      }
    </script>

http://localhost:6060/b.html

  window.onmessage = (e) => {
    console.log(e.data) //打印出hello
    e.source.postMessage('hi', e.origin) //响应对方数据
 }

WebSocket

  • Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。

  • WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。

  • WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

示例如下:

客户端代码:

 const socket = new WebSocket('ws://localhost:6060');  //ws是WebSocket定义的协议
    socket.onopen = () =>  socket.send('Hello');//向服务器发送数据
    socket.onmessage = (e) =>console.log(e.data);//接收服务器返回的数据

服务器代码:

const express = require('express');
const app = express();
const WebSocket = require('ws');//ws 是一个第三方的 websocket 通信模块
const myWs = new WebSocket.Server({port:6060});
myWs.on('connection',(ws)=> {
  ws.on('message', (data) => {
    console.log(data);  //打印客户端发送过来的数据
    ws.send('我不爱你')
  });
})

window.name + iframe

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

flowchart TB
    浏览器当前页面-window.name默认为空-->|设置window.name='aa'并在当前页打开新链接|浏览器新页面-新页面可以不同源-->|在新页面输出window.name| aa
    
    subgraph 浏览器当前页面-window.name默认为空
   
    end
    subgraph 浏览器新页面-新页面可以不同源

    
    end
    subgraph aa
  
    end


所以我们a和b两个页面想要跨域通信可以利用iframe+window.name,需要利用c页面作为媒介实现,c页面和a页面同源才能实现父子页面的iframe读取操作。

http://localhost:6060/a.html页面:

<iframe src="http://localhost:5050/b.html" frameborder="0" onload="load()" id="iframe"></iframe>
 <script>
    let first = true    //加锁
    function load() {
      if(first){
      // 第1次onload(跨域页)成功后,切换到同域代理页面
        const iframe = document.getElementById('iframe');
        iframe.src = 'http://localhost:6060/c.html';  //设置完毕,iframe再次加载,此时加载的是c页面,a和c同源
        first = false;
      }else{
      // 第2次onload后,此时是iframe是c页面,与a页面同源,a可以读取同域下iframe的window.name中数据
        console.log(iframe.contentWindow.name);
      }
    }
 </script>

http://localhost:5050/b.html页面:

<script>
    window.name = 'hello,I am b'
</script>

location.hash+iframe

实现原理: a.html 想和c.html 跨域通信,可以通过在a.html里嵌套iframe链接到 c.html,而c.html里又有一个iframe,链接到b.html,a和b是同域的。

三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。

flowchart TB
      a.html#hello-->|嵌套|c.html
    c.html-->|localtion.hash为hello-创建iframe链接到b.html#hi| b.html#hi
    b.html#hi-->|将当前localtion.hash传给parent的parent的localtion.hash| a.html#hello
    subgraph a.html#hello
   end

    subgraph b.html#hi
   
    
    end
    subgraph c.html
  
    end


document.domain+iframe

实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。需要注意的是该方式只能用于二级域名相同的情况

比如xueshu.baidu.com/a.html里嵌套一个iframe(链接到tieba.baidu.com/b.html),这种情况可以将两个页面设置同一个domain以实现跨域。

flowchart TB
      xueshu.baidu.com/a.html-->|嵌套|tieba.baidu.com/b.html
    tieba.baidu.com/b.html-->|设置document.domain=baidu.com| baidu.com
    xueshu.baidu.com/a.html-->|设置document.domain=baidu.com| baidu.com

小结

  • 跨域仅存在浏览器,脱离浏览器是不存在跨域的!
  • CORS 支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案!