跨域以及七种解决方案

753 阅读9分钟

1. 什么是跨域?

我们平常在看电影的时候相信大家都看过一个场景,在进出某一个军事基地或者重要地区的时候,通常都需要检查你的身份才能放行。我们先假设进出这个地方时,每个人都有一个代表身份的url地址,那么当你想进入某个地方时,就必须对你的这个url地址进行校验,只有符合要求的才能放行,不符合要求的就会被拒绝,而这个要求就是浏览器的同源策略

跨域:在Web开发中,浏览器出于安全考虑,限制了一个网页的脚本或资源访问不同源(协议、域名、端口)的资源

2. 同源策略

2.1 为什么会有同源策略

同源策略之所以会出现是因为考虑到浏览器的安全问题,有了同源策略之后就可以提升数据安全、服务器安全,减少 XSS,CSRF攻击。

2.2 同源策略是什么

在了解同源策略是什么之前我们先来了解一下url的组成,在一个url中通常由四个部分组成:协议域名端口号路径,接下来我们用一个地址为大家解释一下这四个部分分别是什么。

http://127.0.0.1:5500/cors/client.html

  • 协议:http://
  • 域名:127.0.0.1
  • 端口号:5500
  • 路径:/cors/client.html

在上文中我们了解到了跨域是指不允许不同源之间相互访问,而这个不同源指的是在一个请求中如果发送请求的url和接收请求的url两个url中的协议、域名、端口号这三者其中如果有一个或者几个不同的话就不能访问到资源,会被浏览器拦截,而这就是我们所说的同源策略

同源策略:在一个url中只有协议,域名,端口号都相同才能请求数据。如果是非同源请求发送后,浏览器会拦截响应。

接下来我们来看几个例子:

http://127.0.0.1:5500/cors/client.html  访问  http://127.0.0.1:5500/cors  不跨域(协议、域名、端口号相同)
http://127.0.0.1:8000/cors/client.html  访问  http://127.0.0.1:5500/cors/client.html  跨域(端口号不同)
http://127.0.0.1:5500/cors/client.html  访问  http://127.0.0.2:5500/cors/client.html  跨域(域名不同)
http://127.0.0.1:5500/cors/client.html  访问 fcp://127.0.0.1:5500/cors/client.html  跨域(协议不同)

3. 跨域解决方案

当我们得知了浏览器有同源策略之后,可能就会有同学疑惑了,电脑上一个的端口只能运行一个进程,我们在自己的电脑上前后端进行访问都会进行跨域,我们访问别人的电脑域名肯定不一样,那也会跨域,这样的话我们如何来解决跨域这个问题呢?接下来我们来为大家讲解几种解决方案。

3.1 jsonp

这个方案主要就是靠<script></script>标签中的src属性不会进行跨域这一特点来解决跨域的,接下来我们先来为大家讲解一下大致思路 :

  1. 借助script标签中的src属性不受同源策略限制,来向后端发送请求
  2. 携带一个参数 callback 给到后端
  3. 后端将给前端的数据作为 callback 函数的实参,返回给前端一个 callback 的调用形式
  4. 浏览器接收到 callback 的调用会自动执行全局的callback函数(要求前端全局已经具有callback函数)

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <!-- <link rel="stylesheet" href="http://xxxxx/xxx.css"> -->
</head>
<body>
  <button onclick="handle()">请求</button>
  
  <script>
    function jsonp(url, cb) {
      return new Promise((resolve, reject) => {
        // 创建一个新的script标签
        const script = document.createElement('script')
        
        // 在全局上挂载一个cb方法
        window[cb] = function (data) {
          // console.log(data) // 后端返回的数据
          resolve(data)
        }
        // 设置新创建script标签的src属性值并且向后端传递参数cb
        script.src = `${url}?cb=${cb}`
        
        // 将script添加到body中才会发送请求
        document.body.appendChild(script) 
        
        // 上面代码执行完之后获得这个callback('hello world')
      })
      
    }
    function handle() {
      jsonp('http://localhost:3000', 'callback').then(res => {
        console.log(res) // res是后端返回的数据
      })
    }
  </script>
</body>
</html>

后端代码

const http = require('http');
http.createServer((req, res) => {
  const query = new URL(req.url, `http://${req.headers.host}`).searchParams
  // console.log(query.get('cb')); // 用来获取前端穿过来参数cb的值
  if (query.get('cb'))  {
    const cb = query.get('cb') // 'callback'
    const data = 'hello world' // 传递给前端的数据
    const result = `${cb}("${data}")` // 向前端返回一个函数的调用
    res.end(result)
  }
  // res.end('hello world')
}).listen(3000)

代码执行流程:

  1. 前端点击按钮触发handle函数
  2. jsonp函数进行调用,先创建一个<script>标签,并且在全局window上挂载一个cb方法
  3. 将创建的<script>标签挂载到body上后向后端发送请求并且传递参数cb
  4. 后端解析前端传过来的参数cb,并且向前端返回一个callback函数,将想要传递给前端的数据放入这个函数中携带返回给前端
  5. 前端获取后端传过来的带数据的函数,在全局查找该方法,然后进行调用并且将后端传来的数据进行打印。

缺点:

  • 必须要前后端配合
  • 只能发送get请求(由于需要浏览器读取script的src属性,浏览器只能发送get请求)
  • 不安全,容易受到xss攻击

3.2 cors

首先我们知道跨域之所以会出现,是因为不符合标准而被拒绝访问,那我们后端设置一份白名单对特定的源进行放行就好了,这就是cors方法

后端设置 access-control-allow-origin: '域名白名单',来通知浏览器哪些域名可以跨域访问

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button onclick="handle()">请求</button>
  <script>
    function handle() {
      const xhr = new XMLHttpRequest()
      xhr.open('GET', 'http://localhost:3000', true)
      xhr.send()
      xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && xhr.status === 200) {
          console.log(xhr.responseText)
        }
      }
    }
  </script>
</body>
</html>

readyState的状态码可以去官网

image.png

后端代码

const http = require('http')
const server = http.createServer((req, res) => {
  res.writeHead(200, {
    'access-control-allow-origin': 'http://127.0.0.1:5500', // 允许某个域名跨域
    'access-control-allow-methods': 'GET,POST,PUT,DELETE', // 允许的请求方式
    'access-control-allow-headers': 'x-requested-with,content-type' // 允许的请求头
  })


  res.end('Hello World')
})

server.listen(3000)

tips:如果觉得后端写白名单有点麻烦,我们可以去npm官网上下载cors

3.3 nginx 反向代理

这个方法呢代码不方便展示,我们就跟大家聊一聊大致过程,首先我们知道浏览器是有同源策略的,而服务器之间是不存在同源策略的,所以我们根据这一特点,可以在前端向后端传递数据的过程中间添加一个中转站,这个中转站就是我们所说的nginx反向代理

我们在这个中转站上面允许跨域,这样前端传过来的数据先到这个中转站,然后再由中转站传给后端这样就可以避免了跨域问题。反之,后端向前端传数据也是通过中间的这个nginx反向代理来实现中转效果,从而将数据传回给前端。

有一说一,这个过程大家如果记不住可以把它看成是相亲,中间这个nginx反向代理呢就相当于是媒婆一样,前端后端听说要相亲刚开始肯定都不认识,所以会有点警惕(同源策略),这时候就需要有个媒婆来当中间人,将双方相互介绍认识(nginx反向代理)。

前端服务器和后端服务器不在同一个域名下,前端服务器通过nginx 反向代理来访问后端服务器

下面是过程图

image.png

3.4 node 中间件代理

这个方法其实与nginx反向代理的原理一样,只不过换了一种语言写而已,大家参考上面的nginx反向代理的过程即可

前端服务器和后端服务器不在同一个域名下,前端服务器通过node 中间件访问后端服务器

3.5 websocket

传统的前后端通信: 我们在用传统的http进行前后通讯的时候是单向进行的,这个单向并不是说只能前端向后端发消息或者只能后端向前端发消息,而是说当我们在发送请求时前端向后端发送请求,后端向前端返回响应数据,然后这个http请求过程就结束了,如果后端想要再给前端返回数据,那么就要重新发送http请求了

在了解完了基础的http通信方式之后,接下来我们来聊一聊这小节的主角websocket:

websocket:

  1. 是基于tcp协议的,是双向的,可以从一端发送到另一端,也可以从另一端发送到一端
  2. websocket 是基于tcp协议的,是双向的,可以从一端发送到另一端,也可以从另一端发送到一端
  3. socket 协议一段建立连接,就可以一直保持通信状态,不需要每次都建立连接
  4. 天生就可以跨域

我们通过了解了websocket的一些基础特性之后知道了使用这种通信方式不需要额外的手段,天生就能进行跨域,接下来我们来通过代码进行展示,在使用这种方式之前,我们需要在后端中安装一下websocket,在后端的终端中运行。

npm i ws

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
  <script>
    function WebSocketTest(url, params = {}) {
      return new Promise((resolve, reject) => {
        const socket = new WebSocket(url);
        socket.onopen = () => {
          // 发送信息
          socket.send(JSON.stringify(params));
        }
        // 监听信息事件
        socket.onmessage = (e) => {
          console.log(e.data);
          resolve(e.data)
        }
      })
    }
    WebSocketTest('ws://localhost:3000', {age: 18}).then(res => {
      console.log(res);
    })
  </script>
</body>
</html>

后端代码

const WebSocket = require('ws');
// 在 3000 端口上建立 WebSocket 伺服器
const ws = new WebSocket.Server({ port: 3000 });

let count = 0

// 监听连接事件
ws.on('connection', (obj) => {
  // console.log(obj);
  obj.on('message', (msg) => { // 收到客户端发来的消息
    // console.log(msg.toString());
    obj.send('收到了')
    setInterval(() => {
      count++
      obj.send(count)
    }, 1000)
  })
})

在这里我们用代码实现了后端每隔1s就向前端返回count的数值,这个链接是不会中断的,在这个过程中我们虽然前端向后端发送数据,但是并不会出现跨域问题。

3.6 postMessage

在我们写代码的过程中,跨域问题通常是出现在前后端通信的过程中,但是除了前后端通信之外,我们在前端跟前端通信有时候也会出现跨域问题,这个时候我们就可以通过使用postMessage来解决跨域问题

当父级页面和iframe页面不在同一个域名下,他们之间的数据传输也存在跨域问题,父级页面和iframe页面之间可以通过postMessage来通信

前端index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <h2>首页</h2>
  <iframe id="frame" src="http://127.0.0.1:5500/%E8%B7%A8%E5%9F%9F/postMessage/detail.html" frameborder="0" width="800" height="500"></iframe>

  <script>
    let obj = {name: '张三', age: 19}
    document.getElementById('frame').onload = function () {
      this.contentWindow.postMessage(obj, 'http://127.0.0.1:5500')
      window.onmessage = function (e) { // 接收iframe发送的消息
        console.log(e.data);
      }
    }
  </script>
</body>
</html>

前端detail.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <h3>详情页-----<span id="title"></span></h3>

  <script>
    let title = document.getElementById('title');
    window.onmessage = (e) => { // 接收父级页面发送的消息
      console.log(e);
      title.innerHTML = e.data.age;

      e.source.postMessage('我是详情页', e.origin) // 给父级页面发送消息
    }
  </script>
</body>
</html>

3.7 domain

通过设置document.domain来允许同一主域名下的跨域通信,原理同postMessage一样,但是谷歌禁止了这种方法,这里就不再展示

总结

跨域方案:

  1. jsonp
  2. cors
  3. nginx 反向代理
  4. node 中间件代理
  5. websocket
  6. postMessage
  7. domain(谷歌浏览器禁用)