跨域解决方案汇总

一、跨域和同源策略

1. 什么是跨域?

跨域指的是某个域下的文档或脚本试图访问其他域下的接口或资源。

对于一个网站来说,“协议+ip/域名+端口号”就是它所在的域,比如百度的www服务所在的域就是“https://www.baidu.com”(端口号是默认的443),以它开头的所有资源都属于这个域。

假设你现在搭建了一个web站点,所在的域是https://10.10.66.88:8080,然后你在某个页面引入了百度的logo:

<img src="https://www.baidu.com/img/baidu_jgylogo3.gif"

因为这张图片不在你的服务器上,所以你的页面在加载时需要去百度的服务器上下载这张图片,这就是在进行跨域访问。

常见的跨域访问包括:

  1. 资源跳转,如通过a标签的跳转、重定向等
  2. 资源嵌入,如通过<link>、<script>、<iframe>、@font-face()等嵌入的其他网站的资源
  3. 脚本请求,如通过ajax调用其他域下的接口

2. 同源策略

由于跨域访问非静态资源很容易给网站带来安全威胁(如XSS、CSFR攻击等),因此早在web诞生之初,就由Netscape公司提出了同源策略,来限制网站的跨域访问。

同源策略限制以下几种跨域访问:

  1. 读取其他域下的cookie、localStorage、sessionStorage和indexDB数据
  2. 获取其他域下的DOM或JS对象
  3. 通过ajax请求其他域下的接口

同源策略不限制静态资源的访问(静态资源是指HTML文件、CSS样式文件和脚本文件),也就是说,你可以在你的页面内通过<iframe>,<link>,<script>嵌入不属于你自己网站的页面、样式和脚本。

需要特别强调的是,同源策略并不是http(s)协议所要求的,它只是浏览器为了保证web安全而制定的策略(也就是说,如果你不使用浏览器作为http代理去访问服务器,那么同源策略是无效的)。

3. 同源策略带来的问题

一方面,同源策略是浏览器为web安全提供的最核心的功能之一,另一方面,它也是对开发者的一个相当大的限制。由于现在大多数网站的架构都比较复杂,需要依靠多个域下的服务来支撑,而同源策略会导致大多数的跨域访问不可用,这严重影响了网站的开发。

同源策略的限制主要表现在两方面,一是前端的页面通信,二是跨域请求。前端的页面通信指的是,父页面与跨域的iframe之间不能相互访问对方的DOM对象、js对象、变量、方法等。在跨域请求方面的表现是,浏览器将拦截跨域的ajax请求,导致跨域请求失败。

目前主要存在下面9种跨域解决方案,我把他们分为两类,分别是与iframe通信相关的和与跨域请求相关的。

二、跨域解决方案

1. iframe通信类

(1). 修改document.domain

这个方案的前提是,两个页面必须位于同一个基础主域,也就是两者的一级域名和二级域名必须相同。举个例子,https://www.baidu.comhttps://image.baidu.com就是具有相同基础主域的两个域。这时可以在两个页面内将自己的域设置为基础主域,这样它们就变成了同域的页面,如:

父窗口:(https://www.baidu.com/index.html)

document.domain = 'baidu.com';  //修改所在的域
var name = "夕山雨";

iframe子窗口:(https://image.baidu.com/index.html)

document.domain = 'baidu.com';  //现在该iframe与父页面在同一个域

console.log(window.parent.name); //可以访问父页面的变量了

修改了domain之后,父子页面就变成了同域的,不再受同源策略限制。需要注意的是,domain不能修改一、二级域名,因此一、二级域名不同的域不能使用该方案。

(2). 设置location.hash

通过hash值,父页面可以直接向子页面传递少量参数。比如:

父页面:

<iframe id="iframe" src="">
<script>
  var name = "Carter";
  document.getElementById("iframe").src =
        "https://www.baidu.com#name=Carter"
</script>

这样在子页面就能通过window.onhashchange监听到hash值变化,并通过window.location.hash得到传入的name参数。如果需要传入其他参数,只需要重新拼接hash值,并给src赋值即可。

不过仅仅这样,无法实现子页面向父页面通信。为此,需要在子页面内再嵌入一个iframe,并且这个iframe与父页面同域。如:

父页面a.html:(https://www.parent.com/a.html)

<iframe id="iframe" src="https://www.child.com/b.html#name=123">

<script>
  function getMessageFromB(msg){
    console.log(msg);
  }
</script>

子页面b.html:(https://www.child.com/b.html)

<iframe id="b" src="">

<script>
  window.onhashchange = function(){
    console.log(location.hash);  // "#name=123"
    document.getElementById("b").src = 
        "https://www.parent.com/c.html#age=24";
  }
</script>

c.html:(https://www.parent.com/c.html)

<script>
  window.onhashchange = function(){
    window.parent.parent.getMessageFromB(location.hash);
  }
</script>

b页面作为a页面的子页面,无法直接向其发送数据,因此它在内部嵌入了一个与a页面同域的c页面,然后把需要发送的数据通过hash值发送到c页面。由于c页面与a页面是同域的,因此它可以通过window.parent.parent.getMessageFromB调用a页面的方法,将数据发送给a页面,间接实现b.html向a.html的通信。大致过程如下:
在这里插入图片描述

(3). 设置window.name

每个iframe内部都有一个window对象。一般来说,当修改iframe的src属性时,页面会重新加载,window上所有的属性值都会重置,但name属性是例外。

window.name被设置后,只要不被手动修改,值是不会变化的。因此可以将子页面需要传递给父页面的参数写入window.name中,然后将子页面的src设置为与父页面同域的页面,然后从iframe内window.name中提取参数。如:
a.html:(http://www.domain1.com/a.html)

<script>
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = 'http://www.domain2.com/b.html';

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 0) {
             // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        } else if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
        }
    };

    document.body.appendChild(iframe);
</script>

proxy.html:(http://www.domain1.com/proxy.html)
只是作为代理,内容为空。

b.html:(http://www.domain2.com/b.html)

<script>
    window.name = 'b页面向a页面发送的数据';
</script>

主要过程为,先在a页面内通过iframe加载b页面,b页面会将需要传递的参数写到window.name中。随后修改iframe的src,使其加载与a页面同域的proxy.html。这时a页面就可以直接从该iframe的window.name属性中读取刚才b页面写入的值。

(4). postMessage

这是前端跨域访问的官方解决方案,也是目前最为简洁和可靠的方案。postMessage使用起来非常方便,直接调用window.postMessage即可向window发送消息,它是window对象上少数几个不受同源策略限制的方法之一。如:

a.html:(http://www.domain1.com/a.html)

<iframe id="iframe" src="https://www.child.com/b.html">

<script>
  let child = document.getElementById("iframe");
  child.contentWindow.postMessage("来自父页面的消息"); //向子页面发送消息

  //监听子页面发送来的消息
  window.onmessage = function(data){
    console.log(data);  //来自子页面的消息
  }
</script>

b.html:(http://www.domain2.com/b.html)

<script>
  window.parent.postMessage("来自子页面的消息");

  //监听其他页面发送来的消息
  window.onmessage = function(data){
    console.log(data);  //来自父页面的消息
  }
</script>

postMessage不仅可以实现父子页面通信,借助父页面,还可以实现任意两个iframe之间的通信。并且无论是否跨域,都可以使用postMessage进行通信。因此它是前端页面通信中比较可靠和通用的方案。

2. 跨域请求类

(1). JSONP

这是最古老的的一种跨域请求解决方案,是早期web开发者智慧的结晶。它的理论依据是同源策略不会限制跨域脚本的加载,所以只要把响应头的contentType参数设置为“text/javascript”,它就会被浏览器接受和执行(浏览器会认为该响应是个脚本文件,而脚本文件被视为静态资源)。而前端需要生成一个临时的script标签来进行请求。如:

let script = document.createElement("script");
script.type = "text/javascript";

script.src = "https://www.other.com/login.do?name=123&callback=handle";
document.body.appendChild(script);

function handle(res){
  console.log(res);
}

后台的login.do设置response的contentType为“text/javascript”,返回的值为字符串:

'handle({status: "ok"})'

这等价于返回了下面这样一个脚本文件:

handle({status: "ok"});

由于是个脚本文件,浏览器将其视为静态文件,因此不会进行拦截。前台收到这个文件后会自动执行脚本,也就是调用handle({status: "ok"}),这样,后端想要传递的数据{status: "ok"}就被传入了handle方法中(注意,在发送请求时我们携带了参数&callback=handle)。

虽然各个框架实现JSONP的写法不一样,但都是基于这个原理,即将url封装进一个script标签去请求脚本,后端返回“text/javascript”类型的响应作为响应脚本,前端收到响应后像普通脚本一样立即执行。

(2). 跨域资源共享(CORS)

这是目前跨域请求最常用的解决方案,也是官方给出的跨域解决方案。它本质上是对同源策略的一种补充。

从根本上来说,同源策略是为了保证服务端数据和资源的安全。而服务端向外提供的接口并不一定会威胁到服务端安全(比如获取天气信息的接口)。同时,即使跨域调用某个接口可能威胁到服务端安全,但如果某个特定的域是受信任的,那么浏览器也不应该限制该域的跨域调用。

因此web标准工作组提出了跨域资源共享策略(CORS),它允许服务端人为指定资源是否可以被跨域访问,以及可以被哪些域跨域访问,这是通过响应头中新增的几个字段(包括Access-Control-Allow-Origin、Access-Control-Allow-Credentials等)实现的。通过CORS,服务端拥有了配置跨域访问权限的能力。

此时浏览器收到跨域调用的返回结果时,不会像之前一样直接拦截,而是先检查这几个参数,如果符合安全条件,该请求就不会被拦截,前端可以正确得到相应结果,反之会提示跨域报错信息。

下面是一个例子:
前端页面:

$.ajax({
  url: "",  //跨域接口地址
  ...
  xhrFields: {
    withCredentials: true;  //设置携带cookie
  },
  crossDomain: true,  //发送跨域请求
  ...
})

后端java:

...
//设置受信任的域,如果允许任何域调用,可以设置为*
response.setHeader("Access-Control-Allow-Origin", "http://www.domain.com");

//允许前端携带cookie,启用此项后,上一项不可设置为*
response.setHeader("Access-Control-Allow-Credentials", "true"); 

// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
...

这样,http://www.domain.com这个域下的页面就可以跨域调用该接口了。

(3). nginx代理

这是目前非常流行的一种跨域解决方案。我们之前说到,同源策略是针对于浏览器的,与http(s)协议无关。这就是说,两台服务器之间通过http(s)协议进行通信永远不会受到同源策略的限制。

基于这个原理,我们在前后端之间增加一台nginx代理服务器,现在客户端的请求全部发送给该代理服务器,然后由代理服务器转发到服务端。那么我们只要保证访问nginx服务器时不存在跨域,浏览器的同源策略就不会起作用。
在这里插入图片描述

(4). nodejs中间件

原理与nginx大致相同,也是配置一台代理服务器,进行请求转发,不过这个服务器是用nodejs编写的真正意义上的服务器。

因为使用CORS策略需要对服务端代码进行改动,这对于庞大而稳定的项目来说工程量是很大的,而且很容易导致不可预知的bug。所以我们把处理跨域的代码迁移到新创建的服务器上,用nodejs中间件来实现。当前端请求到达nodejs服务器时,由nodejs向服务端发送请求(上面我们说到,服务端的通信不受同源策略限制),然后由nodejs来封装响应头,设置Access-Control-Allow-Origin等参数。

大致原理如下:
在这里插入图片描述
使用nodejs作为中间件,不止可以处理跨域,还可以进行接口适配、处理并发、请求预处理等,感兴趣的可以去学习一下。

(5). WebSocket

从我的角度来看,这并不算是一种跨域解决方案。因为同源策略是针对http(s)协议制定的,它对WebSocket所使用的的ws协议本身就不生效,所以自然不存在跨域问题。

不过你也可以把它看做一种跨域的解决方案,因为当你使用http(s)协议无法与服务器进行通信时,将协议切换为ws协议也不失为一个方法(不过这种情况很少见,因为ws协议有自己专门的用途,它并不是用来替代http(s)协议的)。

关于WebSocket的实现不是本文的重点,这里就不再详述了。

总的来说,对于跨域问题,如果是前端页面通信方面的,一般使用postMessage来解决;如果是前后端通信相关的,一般是采用CORS,或通过配置代理服务器来实现。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值