七种前端跨域解决方案

做前端的同学基本都会遇到跨域问题,本文主要是想总结一下目前前端跨域的解决方案。供以后参考。

在此之前大家有没有想过,前端为什么会发生跨域问题?

同源策略

之所以会发生跨域,是因为同源策略的限制。 下表给出了相对http://127.0.0.1:3000同源检测的示例:

URL 结果 原因
http://127.0.0.1:3000/index.html 同源
https://127.0.0.1:3000 跨源 协议不同
http://127.0.0.2:3000 跨源 域名不同
http://127.0.0.1:3001 跨源 端口不同

跨域有什么影响呢?主要以下三点:

  • 存储空间不共享,一个源中的Javascript脚本不能对属于其它源的数据进行读写操作。如Cookie、LocalStorage、IndexedDB。
  • 不能获取 dom 节点
  • 不能进行 Ajax 通信

基于以上几点原因,所以出现了几种规避同源策略的解决方案。

document.domain

这种方案有个限制,即当主域名,协议和端口相同,只有子域名不同时才能使用。

例如:https://tieba.baidu.com下页面的脚本和http://news.baidu.com下页面的脚本可以设置他们的document.domain属性为baidu.com,从而让这两个站点下面的文档看起来像在同源下,然后就可以让每个文档读取另一个文档的属性。

所以这种方案实际应用的并不多。

JSONP

提到 JSONP 跨域,不得不先提到 script 标签,和 img、iframe 标签类似,这些标签是不受同源策略限制的,JSONP 的核心就是通过动态加载 script 标签来完成对目标 url 的请求。

前端代码示例:(源为 http://127.0.0.1:3001)

function handleResponse(res) {
  console.log(res) // {text: "jsonp"}
}

const script = document.createElement('script')
script.src = 'http://127.0.0.1:3000?callback=handleResponse'
document.head.appendChild(script)

注意URL后面的?callback=handleResponse,这个 callback 后面跟着的 handleResponse 即回调函数名(可以任意取)。 当服务端接收到这个参数后会拼接成形如 handleResponse(JSON) 的形式返还给前端,故称之为JSONP (JSON with Padding)。 这时候浏览器就会自动调用我们事先定义好的 handleResponse 函数。

缺点:

  • 它没有关于JSONP调用的错误处理,一旦回调函数调用失败,浏览器会以静默失败的方式处理。
  • 它只支持GET请求,这是由于该技术本身的特性所决定的。

因此,对于一些需要对安全性有要求的跨域请求,JSONP的使用需要谨慎一点了。

CORS 跨域

CORS(Cross-Origin Resource Sharing) 可以理解为加强版的 Ajax。它的核心思想即前端与后端进行 Ajax 通信时,通过自定义 HTTP 头部设置从而决定请求或响应是否生效。

比如前端代码(url 为 http://127.0.0.1:3001)写了段 Ajax,代码如下:

$.ajax({
	type: "GET",
	url: "http://127.0.0.1:3000",
	success: function(data) {
      console.log("ajax success.", data);
	},
	error: function(error) {
	  console.log("ajax error.", error);
	}
});

这个时候控制台中会报以下错误:

Failed to load http://127.0.0.1:3000/: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1:3001' is therefore not allowed access.

这时候就需要在服务端设置字段 Access-Control-Allow-Origin,它的作用就是设置允许来自什么源的请求。 如果值设置为 *,表明允许来自任意源的请求。服务端代码示例如下:

http.createServer((req, res) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:3001') // 设置允许来自 http://127.0.0.1:3001 源的请求
})

简单请求以及非简单请求

CORS 分为简单请求以及非简单请求。可以这么区分,如果请求方法为 POST、GET、HEAD 时为简单请求, 其它方法如 PUT、DELETE 等为非简单请求,如果是非简单请求的话, 可以在 chrome 的 Network 中看到多了一次 Request Method 为 OPTIONS 的请求。如下图:

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

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

服务端代码示例如下:

http.createServer((req, res) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:3001')
  res.setHeader('Access-Control-Allow-Methods', 'http://127.0.0.1:3001')
})

相较于JSONP只支持GET请求,CORS支持所有类型的HTTP请求。所以CORS也是目前主流的跨域解决方案。

hash + iframe

在文章最开始提到过 iframe 标签也是不受同源策略限制的标签之一,hash + iframe 的跨域核心思想就是, 在 A 源中通过动态改变 iframe 标签的 src 的哈希值, 在 B 源中通过 window.onhashchange 来捕获到相应的哈希值。思路不难直接上代码:

A 页面代码示例(源为 http://127.0.0.1:3000)

<body>
  <iframe src="http://127.0.0.1:3001"></iframe>
  <script>
    const iframe = document.getElementsByTagName('iframe')[0]
    iframe.setAttribute('style', 'display: none')
    const obj = {
      data: 'hash'
    }
    iframe.src = iframe.src + '#' + JSON.stringify(obj) // ① 关键语句
  </script>
</body>

B 页面代码示例(源为 http://127.0.0.1:3001)

window.onhashchange = function() { // ① 关键语句
  console.log('来自 page2 的代码 ' + window.location.hash) // 来自 page2 的代码 #{"data":"hash"}
}

刷新 A 页面,可以发现在控制台打印了如下字段,至此实现了跨域。

来自 page2 的代码 #{"data":"hash"}

这种方式进行跨域优点是支持页面和页面间的通信,缺点也是只支持 GET 方法和单向的跨域通信。

postMessage

为了实现跨文档传送(cross-document messaging),简称 XDM。 HTML5 给出了一个 api —— postMessage。 postMessage() 方法接收两个参数:发送消息以及消息接收方所在域的字符串。代码示例如下:

A 页面代码示例(源为 http://127.0.0.1:3000)

<body>
  <iframe src="http://127.0.0.1:3001"></iframe>
  <script>
    const iframe = document.getElementsByTagName('iframe')[0]
    iframe.setAttribute('style', 'display: none')
    iframe.onload = function() { // 此处要等 iframe 加载完毕,后续代码才会生效
      iframe.contentWindow.postMessage('a secret', 'http://127.0.0.1:3001')
    }
  </script>
</body>

B 页面代码示例(源为 http://127.0.0.1:3001)

window.addEventListener('message', function(event) {
  console.log('From page1 ' + event.data)
  console.log('From page1 ' + event.origin)
}, false)

刷新 A 页面,可以发现在控制台打印了如下字段,至此实现了跨域。

From page1 a secret
From page1 http://127.0.0.1:3000

这种跨域方式优点是是支持页面和页面间的双向通信,缺点也是只能支持 GET 方法调用。

WebSockets

WebSockets 属于 HTML5 的协议,它的目的是在一个持久连接上建立全双工通信。 由于 WebSockets 采用了自定义协议,所以优点是客户端和服务端发送数据量少,缺点是要额外的服务器。

基础的使用方法如下:

const ws = new WebSocket('ws://127.0.0.1:3000')
ws.onopen = function() {
  // 连接成功建立
}

ws.onmessage = function(event) {
  // 处理数据
}

ws.onerror = function() {
  // 发生错误时触发,连接中断
}

ws.onclose = function() {
  // 连接关闭时触发
}

当然一般我们会使用封装好 WebSockets 的第三方库 socket.io,这里具体就不展开了。

nginx反向代理

前端示例代码(源为 http://127.0.0.1:3001):

$.ajax({
  type: "GET",
  url: "http://127.0.0.1:3000/v1/test",
  success: function(res){..},
  ....
})

由于端口号不同,请求发出后肯定会发生跨域问题。

这时可以通过配置Nginx代理服务进行转发。找到 nginx.conf 文件,在编辑器中打开,配置如下:

http {
    include       mime.types;
    default_type  application/octet-stream;
    server {
        ...

        # 添加如下代码
        # 捕获URL中含有"/api/ms"字符串的请求,拦截后进行转发处理
        location /api/ms
        {
          rewrite             /api/ms/(.*) /$1 break;
          proxy_pass  "http://127.0.0.1:3000";
        }
    }
}

前端部分对应的发送请求的代码如下:

$.ajax({
  type: "GET",
  url: "api/ms/v1/test",
  success: function(res){..},
  ....
})

这种方式应用也比较广泛,但是需要额外起一个Nginx服务。

作者:dykingdy
出处:https://lidaguang1989.github.io/
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。