你不知道的JavaScript--Item33 跨域总结与解决的方法
一、神马是跨域(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,载入时间少了,而且降低了server的流量,例如以下:
<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訪问的。这里我记录我所了解到的跨域的方法:
server端代理,这没有什么可说的。缺点在于,默认情况下接收Ajax请求的服务端是无法获取到的client的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、跨域代理
一种简单的办法。就是把跨域的工作交给server。从后台获取其它站点的数据再返回给前台,也就是跨域代理(Cross Domain Proxy)。
这样的方法似乎蛮简单的。改动也不太大。只是就是http请求多了些,响应慢了些。server的负载重了些~
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页面里的代码非常easy,就是给当前的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属性问cdnserver中静态文件的路径)。那么就能够使用script标签从server获取数据,请求时加入一个參数为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 = $_GET['callback'];//得到回调函数
$data = array('a','b','c','d');//要返回的数据
echo $callback.'('.json_encode($data).')';//输出
?>
终于那个页面输出的结果是:
所以通过http://www.b.com/b.php?callback=dosomething得到的js文件,就是我们之前定义的dosomething函数,而且它的參数就是我们须要的json数据,这样我们就跨域获得了我们须要的数据。
这样jsonp的原理就非常清楚了,通过script标签引入一个js文件,这个js文件载入成功后会运行我们在url參数中指定的函数,而且会把我们须要的json数据作为參数传入。所以jsonp是须要server端的页面进行相应的配合的。
知道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来自己主动推断哪种方法更为适用 。