一、神马是跨域(Cross Domain)
说白点就是post、get的url不是你当前的网站,域名不同。例如在*aaa.com/a.html*
里面,表单的提交action是bbb.com/b.html
。
不仅如此,www.aaa.com
和aaa.com
之间也属于跨域,因为www.aaa.com
是二级域名,aaa.com
是根域名。
JavaScript出于安全方面的考虑,是不允许跨域调用其他页面的对象的(同源策略 Same-Origin Policy)。
特别注意两点:
- 第一,如果是协议和端口造成的跨域问题“前台”是无能为力的,
- 第二:在跨域问题上,域仅仅是通过“URL的首部”来识别而不会去尝试判断相同的ip地址对应着两个域或两个域是否在同一个ip上。“URL的首部”指window.location.protocol +window.location.host,也可以理解为“Domains, protocols and ports must match”。
二、为嘛要跨域
跨域这东西其实很常见,例如我们可以把网站的一些脚本、图片或其他资源放到另外一个站点。例如我们可以使用Google提供的jQuery,加载时间少了,而且减少了服务器的流量,如下:
<script type="text/java script" src="https://aja x.googleapis.com/aj ax/libs/jquery/1.4.2/jquery.min.js"></script>
跨域问题产生的场景
有时候不仅仅是一些脚本、图片这样的资源,我们也会希望从另外的站点调用一些数据(有时候是不得不这样),例如我希望获取一些blog的RSS来生成一些内容,再或者说我在“人人开放平台”上开发一个应用,需要调用人人的数据。
当要在在页面中使用js获取其他网站的数据时,就会产生跨域问题,比如在网站中使用ajax请求其他网站的天气、快递或者其他数据接口时以及hybrid app中请求数据,浏览器就会提示以下错误。这种场景下就要解决js的跨域问题。
然而,很不幸的是,直接用XMLHttpRequest来Get或者Post是不行的,例如我用jQuery的$.get去访问如下主域名 :
$.get("http://flycoder.org/",
{}, function(data){
alert(‘跨域不是越狱:‘+data)
}, "html");
结果如下(总之就是不行啦~FF不报错,但是木有返回数据):
那咋么办捏?(弱弱的说,测试的时候我发现IE访问本地文件时,是可以跨域的,不过这也没啥用~囧~)
三、肿么跨域
在浏览器中,<script>
、<img>
、<iframe>
和<link>
这几个标签是可以加载跨域(非同源)的资源的,并且加载的方式其实相当于一次普通的GET请求,唯一不同的是,为了安全起见,浏览器不允许这种方式下对加载到的资源的读写操作,而只能使用标签本身应当具备的能力(比如脚本执行、样式应用等等)。
最常见的跨域问题是Ajax跨域访问的问题,默认情况下,跨域的URL是无法通过Ajax访问的。这里我记录我所了解到的跨域的方法:
- 服务器端代理,这没有什么可说的,缺点在于,默认情况下接收Ajax请求的服务端是无法获取到的客户端的IP和UA的。
- iframe,使用iframe其实相当于开了一个新的网页,具体跨域的方法大致是,域A打开的母页面嵌套一个指向域B的iframe,然后提交数据,完成之后,B的服务端可以:
●返回一个302重定向响应,把结果重新指回A域;
●在此iframe内部再嵌套一个指向A域的iframe。
这两者都最终实现了跨域的调用,这个方法功能上要比下面介绍到的JSONP更强,因为跨域完毕之后DOM操作和互相之间的JavaScript调用都是没有问题的,但是也有一些限制,比如结果要以URL参数传递,这就意味着在结果数据量很大的时候需要分割传递,甚是麻烦;还有一个麻烦是iframe本身带来的,母页面和iframe本身的交互本身就有安全性限制。
3、 利用script标签跨域,这个办法也很常见,script标签是可以加载异域的JavaScript并执行的,通过预先设定好的callback函数来实现和母页面的交互。它有一个大名,叫做JSONP跨域,JSONP是JSON with Padding的略称。它是一个非官方的协议,明明是加载script,为啥和JSON扯上关系呢?原来就是这个callback函数,对它的使用有一个典型的方式,就是通过JSON来传参,即将JSON数据填充进回调函数,这就是JSONP的JSON+Padding的含义。下面详细介绍一下。
为了更好的讲解和测试,我们可以通过修改hosts文件来模拟跨域的效果,hosts文件在C:\Windows\System32\drivers\etc 文件夹下。在下面加3行:
127.0.0.1 www.a.com
127.0.0.1 a.com
127.0.0.1 www.b.com
3.1、跨域代理
一种简单的办法,就是把跨域的工作交给服务器,从后台获取其他站点的数据再返回给前台,也就是跨域代理(Cross Domain Proxy)。
这种方法似乎蛮简单的,改动也不太大。不过就是http请求多了些,响应慢了些,服务器的负载重了些~
3.2、document.domain+iframe的设置
对于主域相同而子域不同的例子,可以通过设置document.domain的办法来解决。
举www.a.com/a.html和a.com/b.html为例.
思路:只需在a.html中添加一个b.html的iframe,并且设置两个页面的document.domain都为’a.com’(只能为主域名),两个页面之间即可互相访问了,代码如下:
www.a.com/a.html中的script
<!DOCTYPE HTML>
<html>
<head>
<meta name="name" content="content" charset="utf-8">
</head>
<body>
<script type="text/javascript">
document.domain=‘a.com‘;
var ifr = document.createElement(‘iframe‘);
ifr.src = ‘http://a.com/b.html‘;
ifr.style.display = ‘none‘;
document.body.appendChild(ifr);
ifr.onload = function(){
//获取iframe的document对象
//W3C的标准方法是iframe.contentDocument,
//IE6、7可以使用document.frames[ID].document
//为了更好兼容,可先获取iframe的window对象iframe.contentWindow
var doc = ifr.contentDocument || ifr.contentWindow.document;
// 在这里操纵b.html
alert(doc.getElementById("test").innerHTML);
};
</script>
</body>
</html>
备注:某一页面的domain默认等于window.location.hostname。主域名是不带www的域名,例如a.com,主域名前面带前缀的通常都为二级域名或多级域名,例如www.a.com其实是二级域名。 domain只能设置为主域名,不可以在b.a.com中将domain设置为c.a.com。
a.com/b.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title></title>
<script type="text/javascript">
document.domain=‘a.com‘;
</script>
</head>
<body>
<h1 id="test">Hello World</h1>
</body>
</html>
如果b.html要访问a.html,可在子窗口(iframe)中通过window.parent来访问父窗口的window对象,然后就可以为所欲为了(window对象都有了,还有啥不行的),同理子窗口也可以和子窗口之间通信。
于是,我们可以通过b.html的XMLHttpRequest来获取数据,再传给a.html,从而解决跨子域获取数据的问题。
但是这种方法只支持同一根域名下的页面,如果不同根域名(例如baidu.com想访问google.com)那就无能为力了。
问题:
- 1、安全性,当一个站点(b.a.com)被攻击后,另一个站点(c.a.com)会引起安全漏洞。
- 2、如果一个页面中引入多个iframe,要想能够操作所有iframe,必须都得设置相同domain。
3.3、动态script标签(Dynamic Script Tag)
虽然浏览器默认禁止了跨域访问,但并不禁止在页面中引用其他域的JS文件,并可以自由执行引入的JS文件中的function(包括操作cookie、Dom等等)。根据这一点,可以方便地通过创建script节点的方法来实现完全跨域的通信。这种方法也叫“动态脚本注入”。
这种技术克服了XMLHttpRequest的最大限制,也就是跨域请求数据。直接用JavaScript创建一个新的脚本标签,然后设置它的src属性为不同域的URL。
www.a.com/a.html中的script
var dynScript = document.createElement(‘script‘);
dynScript.src = ‘http://www.b.com/b.js‘;
dynScript.setAttribute("type", "text/javascript");
document.getElementsByTagName(‘head‘)[0].appendChild(dynScript);
通过动态标签注入的必须是可执行的JavaScript代码,因此无论是你的数据格式是啥(xml、json等),都必须封装在一个回调函数中。一个回调函数如下:
www.a.com/a.html中的script
function dynCallback(data){
//处理数据, 此处简单示意一下
alert(data.content);
}
在这个例子中,www.b.com/b.js需要将数据封装在上面这个dynCallback函数中,如下:
dynCallback({content:‘来自b.com/b.js的消息Hello World!‘});
我们看到了让人开心的结果,Hello World~
不过动态脚本注入还是存在不少问题的,下面我们拿它和XMLHttpRequest来对比一下:
可以看出,动态脚本注入还是有不少限制,只能使用Get,不能像XHR一样判断Http状态等。
而且使用动态脚本注入的时候必须注意安全问题。因为JavaScript没有任何权限与访问控制的概念,通过动态脚本注入的代码可以完全控制整个页面,所以引入外部来源的代码必须多加小心。
3.4 利用iframe和location.hash
这个办法比较绕,但是可以解决完全跨域情况下的脚步置换问题。原理是利用location.hash来进行传值。
- ww.a.com下的a.html想和www.b.com下的b.html通信(在a.html中动态创建一个b.html的iframe来发送请求);
- 但是由于“同源策略”的限制他们无法进行交流(b.html无法返回数据),于是就找个中间人:www.a.com下的c.html(注意是www.a.com下的);
- b.html将数据传给c.html(b.html中创建c.html的iframe),由于c.html和a.html同源,于是可通过c.html将返回的数据传回给a.html,从而达到跨域的效果。
三个页面之间传递参数用的是location.hash(也就是www.a.html#sayHello后面的’#sayHello’),改变hash并不会导致页面刷新(这点很重要)。
具体代码如下:
www.a.com/a.html
//通过动态创建iframe的hash发送请求
function sendRequest(){
var ifr = document.createElement(‘iframe‘);
ifr.style.display = ‘none‘;
//跨域发送请求给b.html, 参数是sayHello
ifr.src = ‘http://www.b.com/b.html#sayHello‘;
document.body.appendChild(ifr);
}
//获取返回值的方法
function checkHash() {
var data = location.hash ?
location.hash.substring(1) : ‘‘;
if (data) {
//处理返回值
alert(data);
location.hash=‘‘;
}
}
//定时检查自己的hash值
setInterval(checkHash, 2000);
window.onload = sendRequest;
www.b.com/b.html
function checkHash(){
var data = ‘‘;
//模拟一个简单的参数处理操作
switch(location.hash){
case ‘#sayHello‘: data = ‘HelloWorld‘;break;
case ‘#sayHi‘: data = ‘HiWorld‘;break;
default: break;
}
data && callBack(‘#‘+data);
}
function callBack(hash){
// ie、chrome的安全机制无法修改parent.location.hash,
// 所以要利用一个中间的www.a.com域下的代理iframe
var proxy = document.createElement(‘iframe‘);
proxy.style.display = ‘none‘;
// 注意该文件在"www.a.com"域下
proxy.src = ‘http://www.a.com/c.html‘+hash;
document.body.appendChild(proxy);
}
window.onload = checkHash;
www.a.com/c.html
//因为c.html和a.html属于同一个域,
//所以可以改变其location.hash的值
//可通过parent.parent获取a.html的window对象
parent.parent.location.hash = self.location.hash.substring(1);
可能有人会有疑问,既然c.html已经获取了a.html的window对象了,为何不直接修改它的dom或者传递参数给某个变量呢?
原因是在c.html中修改 a.html的dom或者变量会导致页面的刷新,a.html会重新访问一次b.html,b.html又会访问c.html,造成死循环……囧呀~
所以只能通过location.hash了。这样做也有些不好的地方,诸如数据容量是有限的(受url长度的限制),而且数据暴露在url中(用户可以随意修改)……
3.5、postMessage(html5)
HTML5中最酷的新功能之一就是 跨文档消息传输Cross Document Messaging。下一代浏览器都将支持这个功能:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 。 Facebook已经使用了这个功能,用postMessage支持基于web的实时消息传递。
otherWindow.postMessage(message, targetOrigin);
- otherWindow: 对接收信息页面的window的引用。可以是页面中iframe的contentWindow属性;window.open的返回值;通过name或下标从window.frames取到的值。
- message: 所要发送的数据,string类型。
- targetOrigin: 用于限制otherWindow,“*”表示不作限制
- a.com/index.html中的代码:
<iframe id="ifr" src="http://www.b.com/b.html"></iframe>
<script>
window.onload = function() {
var ifr = document.getElementById(‘ifr‘);
// 若写成‘http://www.c.com‘就不会执行postMessage了
var targetOrigin = ‘http://www.b.com‘;
ifr.contentWindow.postMessage(‘sayHello‘, targetOrigin);
};
b.com/b.html中的代码:
//通过message事件来通信,实在太爽了
window.addEventListener(‘message‘, function(e){
// 通过origin属性判断消息来源地址
if (e.origin == ‘http://www.a.com‘ &&
e.data==‘sayHello‘) {
alert(‘Hello World‘);
}
}, false);
3.5 使用window.name来进行跨域
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。
比如:有一个页面www.a.com/a.html它里面有这样的代码:
<script type="text/javascript">
window.name = "我是a.html的window.name";
setTimeout(function(){
window.location = ‘b.html‘;
},3000);
再看看www.a.com/b.html页面的代码:
<script type="text/javascript">
alert(window.name);
</script>
我们看到在b.html页面上成功获取到了它的上一个页面a.html给window.name设置的值。如果在之后所有载入的页面都没对window.name进行修改的话,那么所有这些页面获取到的window.name的值都是a.html页面设置的那个值。当然,如果有需要,其中的任何一个页面都可以对window.name的值进行修改。注意,window.name的值只能是字符串的形式,这个字符串的大小最大能允许2M左右甚至更大的一个容量,具体取决于不同的浏览器,但一般是够用了。
上面的例子中,我们用到的页面a.html和b.html是处于同一个域的,但是即使a.html与b.html处于不同的域中,上述结论同样是适用的,这也正是利用window.name进行跨域的原理。
下面就来看一看具体是怎么样通过window.name来跨域获取数据的。还是举例说明。
比如有一个www.a.com/a.html页面,需要通过a.html页面里的js来获取另一个位于不同域上的页面www.b.com/b.html里的数据。
b.html页面里的代码很简单,就是给当前的window.name设置一个a.html页面想要得到的数据值。b.html里的代码:
<script type="text/javascript">
window.name ="我就是页面a.html想要的数据,所有可以转化成字符串的数据都可以在这里使用,比如一个json数据";
</script>
那么在a.html页面中,我们怎么把data.html页面载入进来呢?显然我们不能直接在a.html页面中通过改变window.location来载入data.html页面,因为我们想要即使a.html页面不跳转也能得到data.html里的数据。答案就是在a.html页面中使用一个隐藏的iframe来充当一个中间人角色,由iframe去获取data.html的数据,然后a.html再去得到iframe获取到的数据。
充当中间人的iframe想要获取到data.html的通过window.name设置的数据,只需要把这个iframe的src设为www.cnblogs.com/data.html就行了。然后a.html想要得到iframe所获取到的数据,也就是想要得到iframe的window.name的值,还必须把这个iframe的src设成跟a.html页面同一个域才行,不然根据前面讲的同源策略,a.html是不能访问到iframe里的window.name属性的。这就是整个跨域过程。
看下a.html页面的代码:
<script type="text/javascript">
function getData(){
var ifr = document.getElementById(‘proxy‘);
ifr.onload = function(){//这个时候a.html与ifr已经是同源了,可以相互访问
var data= ifr.contentWindow.name;//获取iframe里的数据,也就是data.html页面设置的数据
alert(data);//成功获得了数据。
}
ifr.src=‘about:blank‘;//这里的about:blank可以是随便的一个页面,只要与a.html同源就可以,目的是让a.html可以访问到iframe里的数据。
}
</script>
<iframe id="proxy" src="http://www.b.com/b.html" style="display: none" onload="getData()"></iframe>
上面的代码只是最简单的原理演示代码,你可以对使用js封装上面的过程,比如动态的创建iframe,动态的注册各种事件等等,当然为了安全,获取完数据后,还可以销毁作为代理的iframe。网上也有很多类似的现成代码,有兴趣的可以去找一下。
通过window.name来进行跨域,就是这样子的。
3.6 通过jsonp跨域
在js中,我们直接用XMLHttpRequest请求不同域上的数据时,是不可以的。但是,在页面上引入不同域上的js脚本文件却是可以的,jsonp正是利用这个特性来实现的。
json≠jsonp
原理
jsonp解决跨域问题的原理是,浏览器的script标签是不受同源策略限制(你可以在你的网页中设置script的src属性问cdn服务器中静态文件的路径)。那么就可以使用script标签从服务器获取数据,请求时添加一个参数为callbakc=?,?号时你要执行的回调方法。
比如,有个www.a.com/a.html页面,它里面的代码需要利用ajax获取一个不同域上的json数据,假设这个json数据地址是http://www.b.com/b.php,那么a.html中的代码就可以这样:
<script type="text/javascript">
function dosomething(jsondata){
//处理json数据
}
</script>
<script src="http://www.b.com/b.php?callback=dosomething"></script>
我们看到获取数据的地址后面还有一个callback参数,按惯例是用这个参数名,但是你用其他的也一样。当然如果获取数据的jsonp地址页面不是你自己能控制的,就得按照提供数据的那一方的规定格式来操作了。
因为是当做一个js文件来引入的,所以http://www.b.com/b.php返回的必须是一个能执行的js文件,所以这个页面的php代码可能是这样的:
<?php
$callback</span> = <span class="hljs-variable">$_GET[‘callback‘];//得到回调函数
$data = array(‘a‘,‘b‘,‘c‘,‘d‘);//要返回的数据
echo $callback</span>.<span class="hljs-string">‘(‘</span>.json_encode(<span class="hljs-variable">$data).‘)‘;//输出
?>
最终那个页面输出的结果是:
所以通过http://www.b.com/b.php?callback=dosomething得到的js文件,就是我们之前定义的dosomething函数,并且它的参数就是我们需要的json数据,这样我们就跨域获得了我们需要的数据。
这样jsonp的原理就很清楚了,通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的函数,并且会把我们需要的json数据作为参数传入。所以jsonp是需要服务器端的页面进行相应的配合的。
知道jsonp跨域的原理后我们就可以用js动态生成script标签来进行跨域操作了,而不用特意的手动的书写那些script标签。如果你的页面使用jquery,那么通过它封装的方法就能很方便的来进行jsonp操作了。
<script type="text/javascript">
$getJSON(‘http://www.b.com/b.php?callback=?‘,function(jsondata){
//处理获得的json数据;
});
原理是一样的,只不过我们不需要手动的插入script标签以及定义回掉函数。jquery会自动生成一个全局函数来替换callback=?中的问号,之后获取到数据后又会自动销毁,实际上就是起一个临时代理函数的作用。$.getJSON
方法会自动判断是否跨域,不跨域的话,就调用普通的ajax方法;跨域的话,则会以异步加载js文件的形式来调用jsonp的回调函数。
四、总结
研究了几天,虽然对多种跨域方法都有所了解了,但是真要投入应用还是明显不够的(还是需要借助一些js库)。
每种方法都有其优缺点,使用的时候其实应该将多种跨域方法进一步封装一下,统一调用的接口,利用js来自动判断哪种方法更为适用 。