跨域解决方案汇总

什么是跨域

首先我们要知道所谓不同域就是协议、域名、端口这三者其中一个不一样。比如说:

  1. http://www.a.comhttps://www.a.com 不同域,因为协议不同
  2. http://m.a.comhttp://www.a.com 不同域,因为域名不同(子域名不同)
  3. http://www.a.comhttp://www. b.net 不同域,因为域名不同(主域名不同)
  4. http://www.a.com:80http://www.a.com:443 不同域,因为端口不同
  5. http://www.a.comhttp://www.a.com/login 同域,因为除了请求资源地址不同以外,其他都相同

而跨域自然就是指某域下请求另一个域的资源。Javascript为了安全,提出了同源策略,限制了跨域。同源策略最早是Netscape为了cookie而创建的规则,它要求某些操作只能在同域下进行。受到同源策略影响的是下面这三类:

  1. 获取cookie、localstorage、IndexDB。
  2. DOM操作
  3. Ajax请求

前两点很容易理解,如果可以跨域获取localstorage岂不是很容易就能盗取信息?如果能跨域操作DOM岂不是能插入广告或者获取密码框的值?

至于Ajax请求为什么不能跨域,我们可以想象一下这个场景:我的电脑能同时访问内网和网外,有一天我点开了一个钓鱼网站,如果没有同源策略的话钓鱼网站就能发起一个跨域Ajax请求,通过我获取到内网的信息。因此我们需要限制跨域Ajax请求。

解决跨域问题的方法

在实际开发中我们经常需要发起一些跨域请求,这就需要我们想办法规避同源策略的限制。解决跨域问题的方法有很多,我这里介绍最常见的几种方法。

jsonp跨域

jsonp利用了<script>标签可以跨域的特点,我们可以在a域下请求一个b域的脚本文件,而这个脚本文件里面就带上了我们需要的信息。

1
2
3
4
5
6
7
8
// a域下的脚本
window.cb1 = function(res){
console.log(res);
}
var script = document.createElement("script");
script.src = "http://www.b.com/json?callback=cb1";
document.body.appendChild(script);

1
2
// 请求回来的脚本内容
cb1({val:"hello world"}); // 也就是运行这个callback

通过这个例子可以看出,jsonp最大的缺点就是不支持post请求。

另外,jQuery等都能很方便地实现jsonp,大家可以查阅一下相关api。

跨域资源共享(CROS)

这个方法非常简单,在不涉及cookie的情况下只需要服务端设置一个Access-Control-Allow-Origin的响应头。如果需要带上cookie,则前端需要设置xhr.withCredentials = true;
CROS跨域与jsonp相比,他可以发起get和post请求,在信息量大的时候尤其有优势。目前这个方法已经得到主流现代浏览器的支持,没有意外的话会成为将来的主流。

关于Access-Control-Allow-Origin需要补充两点:

  1. 他的参数只能是*或者是指定的域名,
  2. 有时候我们需要接收主域名为a.com的所有域名的请求,类似实现*.baidu.com这种。我们可以通过后台判断请求头的origin,判断之后动态的往Access-Control-Allow-Origin里写入这个域名。而无需手动将所有域名一个个添加进Access-Control-Allow-Origin

location.hash + iframe

主要是利用了两个特性:

  1. 父窗口和子窗口可以相互操作对方的location.hash
  2. hash的变化不会引起页面重新加载

这个方法的过程大概是:

  1. 让子窗口在b域下,父窗口在a域下
  2. 父窗口修改子窗口的hash值,通过hash值将请求信息传给子窗口
  3. 子窗口坚挺到hash变化之后,在b域发起请求
  4. 子窗口拿到返回之后,修改父窗口的hash值,通过hash值将返回信息传回给父窗口
  5. 父窗口监听到hash变化之后获取hash携带的返回信息并作出相应

下面简单展示一个例子,为了简单起见省掉了父窗口修改子窗口的hash值这一步,直接在加载iframe的时候就发起请求。

1
2
3
4
5
6
7
8
// 父窗口,在http://www.a.com下
var iframe = document.createElement('iframe')
iframe.src = 'http://www.b.com/ifram.html'
document.body.appendChild(iframe)
window.onhashchange = function () {
console.log(location.hash)
}
1
2
3
4
5
6
7
8
9
10
// 子窗口,在http://www.b.com下
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState === 4 && xhr.status === 200){
var res = JSON.parse(xhr.responseText);
window.parent. parent.location.href = `http://www.a.com#msg=${res.msg}`
}
}
xhr.open('GET', 'http://www.b.com/json', true)
xhr.send(null);

window.name + iframe

这里利用了window.name的两个特性:

  1. 切换到不同页面不同域后,window.name依然存在
  2. 可以给window.name设置很大的值,(2MB)

利用这两个特性我们可以实现跨域,步骤如下:

  1. 先将子窗口切换到b域,
  2. 子窗口在b域请求之后把数据写在window.name中,再切换回a域。
  3. a域下的父窗口通过.contentWindow.name获取数据
1
2
3
4
5
6
7
8
9
// 父窗口,http://www.a.com/
var iframe = document.creatElement("iframe");
iframe.src = "http://www.b.com/iframe.html";
document.body.appendChild(iframe);
var times = 0;
iframe.onload = function(){
if(++times == 2) console.log(JSON.parse(iframe.contentWindow.name));
}
1
2
3
4
5
6
7
8
9
10
// 子窗口
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4 && xhr.status == 200){
window.name = xhr.responseText;
location.href = "http://www.a.com/ifram.html";// 回到a域
}
}
xhr.open("GET","http://www.b.com/json",false);
xhr.send(null);

postMessage

window.postMessage这是HTML5 XMLHttpRequest Level 2中的API,允许跨窗口通信,无论两个窗口是不是在同一个域。

他有三个参数:

  1. message,需要发送的数据,字符串类型。必选
  2. targetOrigin,来指定哪些窗口能接收到消息事件,可以是*也可以是URL。必选
  3. transfer,是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。可选

另外需要注意的是接收方必须要监听message事件,否则会报错。

1
2
3
4
5
6
7
8
// 父窗口
var iframe = document.createElement("iframe");
iframe.url = "http://www.b.com/iframe.html";
document.body.appendChild(iframe);
window.addEventListener('message', function(e) {
console.log(JSON.parse(e.data))
}, false);
1
2
3
4
5
6
7
8
9
// 子窗口
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4 && xhr.status == 200){
parent.postMessage(xhr.responseText, '*')
}
}
xhr.open("GET","http://www.b.com/json",false);
xhr.send(null);

其他

除了上面提到过的五种跨域方法以外,还有一些其他方法:

  • nginx代理跨域
  • Nodejs中间件代理跨域
  • WebSocket协议跨域

这些方法涉及了Nginx、NodeJs等知识,我暂时还搞不定,就不赘述了。

先挖个坑,日后再补。— 2018.2.9

Reference