跨域问题及解决方案

说到javascript跨越问题,首页必然需要引入一个概念。那就是为什么会存在跨域问题?又该如何解决呢?

一、同源策略(same-origin policy)

在web应用安全模型里同源策略是一个很重要的概念。它是由Netscape提出的一个著名的安全策略,现在所有的可支持javascript的浏览器都会使用这个策略。

同源策略,又称为单源策略,它限制了一个源(origin)中加载文本或脚本与来自其它源(origin)中资源的交互方式。

Mozilla认为两个页面它们拥有相同的协议、端口(如果指明)、主机名,那么这两个页面就拥有相同的源。

下面来看一个例子,假设有一个url地址为: http://www.a.com/page/a.html的同源检测示例:

URL 非IE浏览器结果 IE浏览器结果 原因
http://www.a.com/page/b.html 成功 成功 -
http://www.a.com/app/c.html 成功 成功 -
http://username:password@www.a.com/page/b.html 成功 成功 -
http://www.a.com:81/page/d.html 失败 成功 端口不同
https://www.a.com/page/e.html 失败 失败 协议不同
http://www.b.a.com/page/c.html 失败 失败 主机不同
http://www.c.com/page/c.html 失败 失败 主域不同

从上表不难看出,对于IE来说在处理同源策略上有一些不同:

1、端口:IE未将端口号加入到同源策略的组成部分之中,因此 http://a.com:81/index.htmlhttp://a.com/index.html 属于同源并且不受任何限制。

2、授信范围(Trust Zones):两个相互之间高度互信的域名,如公司域名(corporate domains),不遵守同源策略的限制。

除这些指定的URL之外,也常常会有来自about:blank,javascript:和data:URLs中的内容,它们遵循源继承的原则,继承将其载入的文档所指定的源,因为它们的URL本身未指定任何关于自身源的信息。

二、Javascript跨域解决方案

1、设置Domain

页面可以改变本身的源,但会受到一些限制。脚本可以设置document.domain 的值为当前域的一个后缀

在同源策略中有一个例外,脚本可以设置 document.domain 的值为当前域的一个后缀,如果这样做的话,短的域将作为后续同源检测的依据。例如,假设在 http://b.a.com/page/a.html 中的一个脚本执行了下列语句:

1
document.domain = "a.com"

这条语句执行之后,页面将会成功地通过对 http://a.com/page/a.html 的同源检测。而同理,a.com 不能设置 document.domain 为 b.com.

浏览器单独保存端口号。任何的赋值操作,包括document.domain = documen.domain都会以null值覆盖掉原来的端口号。因此a.com:8080页面的脚本不能仅通过设置document.domain = “a.com”就能与company.com通信。赋值时必须带上端口号,以确保端口号不会为null。

注意:使用document.domain来安全是让子域访问其父域,需要同时将子域和父域的document.domain设置为相同的值。必须要这么做,即使是简单的将父域设置为其原来的值。没有这么做的话可能导致授权错误。

  • 举个例子

    a.taobao.com嵌入一个b.taobao.com的页面,并且需要做高度自适应,那么可以这么来做:

    • 在a页面中设置domain并定义autoHeight方法(用处为改变iframe的高度)
1
document.domain = "taobao.com"
- 在b页面,页面加载之后或者所有涉及到高度变化的事件中,调用a页面的方法:
1
2
3
4
5
function fixHeight(){
document.domain = 'taobao.com';
var oHeight = document.documentElement.scrollHeight;
window.parent.autoHeight(oHeight); //调用a页面的autoHeight方法,在该方法中去改变iframe的高度
}

2、使用代理页

如果主域名也不同该怎么做呢?如:a.com/index.html和b.com/iframe.html

  • 有一种办法是采用代理页的方式

    即在b.com下建立一个b.com/proxy.html代理页面,还是以高度自适应为例来说明:

    a页面iframe该proxy页面,并将高度以hash值的方式填到这个嵌入proxy.html的iframe的src中去
    在proxy.html中去拿到这个hash中的高度值,并去操作父父层页面的高度
    iframe cross domain

    代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#set_height.js
(function(win, doc){
win.onload = function () {
var url = "http://a.com/proxy.html";
var ifmProxy = doc.getElementById("ifmProxy"); //iframe target
var hs; //height
var hsLastTime = 0; //height last time
var startTime = 0; //count
var loadIframeStart = win.setInterval( function () {
setHeight();
}, 500);
function setHeight() {
startTime++;
//get current height
hs = doc.documentElement.scrollHeight;
//update the hs height
if(hs !== hsLastTime) {
ifmProxy.src = url + "#" + hs;
hsLastTime = hs;
}
//clearCount and restart
if(startTime >= 500) {
clearInterval(loadIframeStart);
startTime = 0;
loadIframeStart = win.setInterval( function() {
setHeight();
}, 500)
}
}
};
})(window, document);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#proxy.html
<script>
try{
var ifmAnalyst = parent.parent.document.getElementById('isvIframeCon'); //目标iframe
var hs; //获取的高度
var hsLastTime = 0; //上一次获取到的高度
var startTime = 0; //计时器初始值
var loadIframeStart = window.setInterval( function () {
updateHeight();
}, 500);
function updateHeight() {
startTime++;
//获取当前的高度值
hs = window.location.hash;
//当高度改变则去更新iframe的高度同时将hsLastTime更新
if(hs !== hsLastTime) {
ifmAnalyst.style.height = hs.split("#")[1] + "px";
hsLastTime = hs;
}
//当达到一定的次数,清理一次计时器,并重新开始监听
if(startTime >= 500) {
clearInterval(loadIframeStart);
startTime = 0;
loadIframeStart = window.setInterval( function() {
updateHeight();
}, 500)
}
}
} catch(ex) {
}
</script>

3、JSONP(创建script标签形式)

所谓JSONP的方式简单来说就是利用script标签不受同源策略的限制这一特性。

我们通过script标签的形式把想要请求的地址作为src传入,从而获得相应的文件或者JSON数据。

a.com下的数据fn({“name”: “amo”});

b.com下的函数function fn(res){ console.log(res.name); }

下面我们来看下具体来写应该怎样做:

  • 在A.com网站下:
1
2
3
4
5
6
7
8
9
10
function createJs(sUrl){
var oScript = document.createElement('script');
oScript.type = 'text/javascript';
oScript.src = sUrl;
document.getElementByTagName('head')[0].appendChild(oScript);
}
createJs('jsonp.js?callback=fn');
function fn(rs) {
console.log(rs);
}
  • 在B.com的jsonp.js通过拿到这个callback参数,并把它填充到具体的数据中,格式如下
1
fn({"name", "amo"});
这样就相当于页面上是这样的
1
2
3
<script>
fn({"name": "amo"});//fn这个函数的调用
</script>

当然这里只是一个思路的过程,实际JSONP的过程要比这复杂的多,比如说是否onload、回调状态码、格式的校验等。

4、postMessage方式

在HTML5中,提出了工作线程的概念(web workers)。Web Workers 允许开发人员编写能够长时间运行而不被用户所中断的后台程序,去执行事务或者逻辑,并同时保证页面对用户的及时响应。web worker一旦被创建,就可以通过postMessage 向任务池发送任务请求,执行完之后再通过 postMessage 返回消息给创建者指定的事件处理程序 ( 通过 onmessage 进行捕获 )。Web Workers 进程能够在不影响用户界面的情况下处理任务,并且,它还可以使用 XMLHttpRequest 来处理 I/O,但通常,后台进程(包括 Web Workers 进程)不能对 DOM 进行操作。如果希望后台程序处理的结果能够改变 DOM,只能通过返回消息给创建者的回调函数进行处理。

那么利用这个特性,我们怎么来实现跨域之间的通信呢?还是以高度自适应为例。

a.com/parent.html 嵌入 b.com/child.html该如何去获取到b下面的页面高度呢?

在child.html中引入如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(function(win, doc){
win.onload = function () {
var hs; //height
var hsLastTime = 0; //height last time
var startTime = 0; //count
var loadIframeStart = win.setInterval( function () {
setHeight();
}, 500);
function setHeight() {
startTime++;
//get current height
hs = doc.documentElement.offsetHeight;
//update the hs height
if(hs !== hsLastTime) {
window.parent.postMessage({h: hs}, '*'); //指定是向父容器postmessage
hsLastTime = hs;
}
//clearCount and restart
if(startTime >= 500) {
clearInterval(loadIframeStart);
startTime = 0;
loadIframeStart = win.setInterval( function() {
setHeight();
}, 500)
}
}
};
})(window, document);

在parent.html加载完iframe之后引入如下代码

1
2
3
4
5
6
window.addEventListener("message", function(data) {
var ifmAnalyst = document.getElementById('isvIframeCon'),//目标iframe
ifmHeight = data.data.h;
//data.data.h即为高度
ifmAnalyst.style.height = ifmHeight + "px";
});

这种方法虽然很方便,但是是H5的特性,所以在使用前先考虑是否需要兼容低版本浏览器

5、服务器代理

6、flash方式

后两种方式,这里不做介绍。