跨域通信与实验
V1 | 2012-4-5 |
V2 | 2012-5-29 |
V2.1(优化) | 2013-5-13 |
实验文件的使用方法见readme.txt
前言
因为浏览器的同源策略和目前大型web服务的文件分布于各域名,js中跨域通信也越来越多的出现,本文总结和实验一些跨域的方法。
实验代码:source.zip
见source目录,实验代码来自https://github.com/colorhook/crossdomain 并做了一些修改。
实验指导请见各个目录中的README
什么是跨域
什么是同源(同域)
同源策略又名同域策略是浏览器中的主要安全措施。这里的“源”指的是主机名、协议和端口号的组合。
比如:
下表给出了相对http://store.company.com/dir/page.html同源检测的结果:
URL | 结果 | 原因 |
http://store.company.com/dir2/other.html | 成功 | |
http://store.company.com/dir/inner/another.html | 成功 | |
https://store.company.com/secure.html | 失败 | 协议不同 |
http://store.company.com:81/dir/etc.html | 失败 | 端口不同 |
http://news.company.com/dir/other.html | 失败 | 主机名不同 |
在同源策略中有一个例外,脚本可以设置 document.domain 的值为当前域的一个后缀,比如域store.company.com的后缀可以是company.com。如果这样做的话,短的域将作为后续同源检测的依据。例如,假设在 http://store.company.com/dir/other.html 中的一个脚本执行了下列语句:
document.domain = “company.com”; |
这条语句执行之后,页面将会成功地通过对 http://company.com/dir/page.html 的同源检测。而同理,company.com 不能设置 document.domain 为 othercompany.com
可以看到,只要1级域名相通,那么经过一定设置也可以简单通信,但是如果一级域名也不同的话只能利用一些方法进行跨域通信了。
跨域的分类
跨域访问实际上会有很多种情况,我把这里讨论的跨域通信分为两类
一类是跨域iframe访问(页面中的顶页面和iframe中的页面之间的访问)
一类是跨域访问服务器端资源(比如XHR)
条件 | 结果 | |||
源页面 | 目标 | 设置 | iframe访问 | XHR访问 |
a.com | a.com | 可以 | 可以 | |
a.com | s.a.com | 不可以 | 不可以 | |
a.com | s.a.com | domin设为a.com | 可以 | 不可以 |
a.com | b.com | 不可以 | 不可以 |
浏览器基于安全原因,会不允许一些跨域访问,我们就要用下面的方法来实现跨域访问。
跨域实验
先来进行一个实验,来证实一下上面说的内容
(本实验在XHR目录)
www.a.com的一个页面要访问四个域名下的服务器数据,
结果是 只有第一个按钮(获取www.a.com上的一个XML)能执行成功
其他按钮均会收到错误:
跨域通信的方法
新的标准
既然有跨域访问的需求,且不是很小众的要求,那么W3C就会制定相关标准。
XMLHttpRequest Level 2
与服务器通信 | √ |
iframe通信 | × |
标准文档:http://www.w3.org/TR/XMLHttpRequest/
新设计出来的跨域方案是优秀的,IE8是XDomainRequest,Firefox3.5、Safari4、Chrome 2等是沿用原来的XMLHttpRequest对象,它们都拥有一些相同的方法处理各种回调:
函数 | 意义 |
onload | 请求成功时调用。 |
onerror | 请求失败时调用。 |
onabort | 请求中断时调用(使用abort方法) |
因此这跨域请求是非常简单了!
例子程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 | if("1"[0]){//只允许IE8与较新的标准浏览器进入下面逻辑 var xhr=window.XDomainRequest?new XDomainRequest:new XMLHttpRequest; try{ xhr.onload=function(){ //由于返回的JSON过长,我们在演示时把它截短一些 alert([xhr.responseText.slice(0,1000),xhr]); }; xhr.open("GET","http://ss-o.net/json/wedataAutoPagerize.json"); xhr.send(); }catch(e){ alert("请求失败: "+e.message); } } |
其中除了这个“if(“1″[0])”稍微有些玄幻,其他的大家应该可以看得懂。
但是这个跨域程序直接运行是不行的,还需要在服务端进行配置:
跨域资源共享(Cross-Origin Resource Sharing)
标准文档:http://www.w3.org/TR/cors/
对于浏览器来说,COR请求都是Javascript发起的,COR请求有两种:
1、简单的COR请求,它可以直接向外域资源发起请求。它必须仅仅包含简单的方法和头。
2、如果COR包含复杂的方法和头,它需要发出预检验(Preflight)请求,它先向资源服务器发出一个OPTIONS方法、包含“Origin”头的请求。该回复可以控制COR请求的方法,HTTP头以及验证等信息。只有该请求获得允许以后,才会发起真实的外域请求。
简单地说, CORS需要在请求返回的http header中包含以下内容:
设置 | 意义 |
Access-Control-Allow-Origin | 允许跨域请求申请来源中的域名限制,*为不限制 |
Access-Control-Allow-Methods | 允许请求的方法(GET、POST等) |
其他 | 等等 |
下面是一个简单的COR请求:
1 2 3 4 | var client = new XMLHttpRequest(); client.open("GET", "http://bar.org/b") client.onreadystatechange = function() { /* do something */ } client.send() |
假设这个请求所在页面的域是“http://foo.org”。 如果来自“http://bar.org/b”的回复包含这样的头:
1 | Access-Control-Allow-Origin: http://foo.org |
则表明,它允许来自“http://foo.org”的跨域请求。
下面的Javascript会发出预检验请求和真实请求:
1 2 3 4 5 | var client = new XMLHttpRequest(); client.open("GET", "http://bar.org/b") client.setRequestHeader('Content-Type','text/html') client.onreadystatechange = function() { /* do something */ } client.send() |
由于“Content-type: text/html”不是一个简单的头,它会先向”http://bar.org/b”发出一个OPTIONS的HTTP请求。 回复可能包含这样的头:
1 2 3 4 | Access-Control-Allow-Origin: http://foo.org Access-Control-Max-Age: 3628800 Access-Control-Allow-Methods: GET,PUT, DELETE Access-Control-Allow-Headers: content-type |
“Access-Control-Allow-Origin”表明它允许”http://foo.org”发起跨域请求
“Access-Control-Max-Age”表明在3628800秒内,不需要再发送预检验请求,可以缓存该结果
“Access-Control-Allow-Methods”表明它允许GET、PUT、DELETE的外域请求
“Access-Control-Allow-Headers”表明它允许跨域请求包含content-type头
如果预检验请求获得通过,接下来Javascript就会发起真实的COR请求,过程跟简单的COR请求类似。
实验
代码见source\access-control
JSONP
与服务器通信 | √ |
iframe通信 | × |
讨论完了新的标准,也不能忽略不支持新标准的浏览器
Jsonp是广泛支持的一种跨域方法
浏览器虽然限制了Ajax的跨域通信,但允许在页面中插入动态的脚本元素。简单的讲就是从第三方服务器加载js代码是可行的
但加载的js代码同样被视作是从当前域加载的,所以想在第三方的js代码中进行对第三方服务器的ajax调用同样是不行的。
所以可以通过第三方服务器生成动态的js代码来回调本地的js方法,而方法中的参数则由第三方服务器在后台获取,并以JSON的形式填充到JS方法当中,这也就是“JSON with Padding”中“padding”的真正意义。
应用过程当中,请求方(本地)向第三方服务器请求动态JS脚本,并将获取数据后需要回调的函数名以约定好的参数名(如callback等)发送给第三方服务器。
第三方服务器需要为JSONP请求开发相应的API,API中先获取JSONP请求需要的数据,然后以JSON的形式封装,再与请求方的回调函数名拼接在一起,动态生成请求方需要调用的JS代码。
假设这个JSONP服务的URL为http://www.google.com/jsonp,回调的函数名为jsonpFunc,那么可以这样发送JSONP请求: (动态添加到html中)
1 2 | <script type="text/javascript" src="http://www.google.com/jsonp&company=IBM&callback=callback"> </script> |
但实际的开发中,JS库中一般都包含了更便于使用的JSONP方法,例如jquery和kissy。
以jquery为例,jsonp的调用形式如下
1 2 3 4 5 6 | jQuery.getJSON( "http://www.yourdomain.com/jsonp/ticker?symbol=IBM&callback=?", function(data) { alert("Symbol: " + data.symbol + ", Price: " + data.price); } ); |
实验
代码在source/jsonp
关键部分代码(index.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 29 30 31 32 | var getJSONP = function(url, callbackName){ var head = document.getElementsByTagName("head")[0], script = document.createElement("script"); script.src = url + '?callback=' + callbackName; script.charset = "utf-8"; script.onload = script.onreadystatechange = function(){ if (!this.readyState || this.readyState == "loaded" || this.readyState == "complete") { setTimeout(function(){ head.removeChild(script); }, 50); } }; head.appendChild(script); }, holder = document.getElementById('resource_holder'), button = document.getElementById('get_resource'); window.handleJSONP = function(data){ var dumpInfo = 'timestamp: ' + data.timestamp + '<br>count: ' + data.count, record = data.data; for(var i in record){ dumpInfo += '<br>record ' + i + ': {id: ' + record[i].id + ' name: ' + record[i].name +'}'; } holder.innerHTML = dumpInfo; } button.onclick = function(){ holder.innerHTML = ''; getJSONP('http://damon.snsdev.isd.com/gdt/jsonp.php', 'handleJSONP'); } |
Flash URLLoader
与服务器通信 | √ |
iframe通信 | × |
Flash有自己的一套安全策略,服务器可以通过crossdomain.xml文件来声明能被哪些域的SWF文件访问,SWF也可以通过API来确定自身能被哪些域的SWF加载。
当跨域访问资源时,例如从域www.a.com请求域www.b.com上的数据,我们可以借助Flash来发送HTTP请求。首先,修改域www.b.com上的crossdomain.xml(一般存放在根目录,如果没有需要手动创建) ,把www.a.com加入到白名单。其次,通过Flash URLLoader发送HTTP请求,最后,通过Flash API把响应结果传递给JavaScript。
Flash URLLoader是一种很普遍的跨域解决方案,不过需要支持iOS的话,这个方案就无能为力了。
例子程序:(actionScript)
1 2 3 4 5 6 7 8 9 10 11 | public function URLLoaderExample() { var loader:URLLoader = new URLLoader(); configureListeners(loader); var request:URLRequest = new URLRequest("urlLoaderExample.txt"); try { loader.load(request); } catch (error:Error) { trace("Unable to load requested document."); } } |
利用form表单跨域post
现在ajax应用这么广泛,一般的应用都是直接通过异步调用就可以了,但是有些东西必须要使用post,而且是跨域的时候,ajax异步调用的方式就无能为力了。
当然现在也有很多种办法,比如通过flash中转去post,可以post到任何域中,或者是通过嵌入iframe来实现,flash的方式虽然好,但是用户还得下载个swf文件。这里用form和iframe来实现,
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 32 33 34 35 36 37 38 39 40 41 42 | <body> <script type="text/javascript"> function check() { var btn = document.getElementById("test_submit"); var frm = document.forms["test_form"]; var ifm = document.getElementById("test_iframe"); frm.action = "http://xxx.xxx.xxx/post.php"; frm.target = "test_iframe"; frm.submit(); btn.disabled = "disabled"; if(ifm.attachEvent){ // for ie ifm.attachEvent("onload", function(){ btn.disabled = ""; var str = ifm.contentWindow; alert(str.document.body.innerHTML); ifm.src = "about:blank"; ifm.detachEvent("onload", arguments.callee); } }else{ ifm.onload = function(){ btn.disabled = ""; var str = ifm.contentWindow; alert(str.document.body.innerHTML); ifm.src = "about:blank"; ifm.onload = null; } } return false; } </script> <form id="test_form" name="test_form"> <input type="hidden" value="xxx" name="content"> <input id="test_submit" type="submit" value="提交查询内容" name="test_submit"> </form> <iframe id="test_iframe" style="display: none" height="1" width="1" name="test_iframe"></iframe> </body> |
在这里,我通过将需要post的内容写入content的input中,然后点击提交,将form的action设置为目标服务器的url,target设置为iframe的名称,这样就可以实现无刷新的跨域post了,但是由于浏览器防止重复提交的功能,所以如果直接提交到iframe的话,后面你刷新页面的话,浏览器就会提示是否要重复提交,所以这里我们监听iframe的onload事件,当iframe成功load之后,就将iframe的src指向空白页,从而浏览器认为已经跳转到新页面了,刷新也就不会提示重复提交的弹出框了。
这里我们还可以在iframe load成功的时候,通过contenWindow属性来获取服务器的响应,从而可以判断post是否成功。
hash值实现通讯(Fragment Identifier Messaging)
与服务器通信 | iframe通信 |
× | √ |
不同的域之间,JavaScript只能做很有限的访问和操作,其实我们利用这些有限的访问权限就可以达到跨域通信的目的了。
FIM (Fragment Identifier Messaging)就是在这个大前提下被发明的。父窗口可以对iframe进行URL读写,iframe也可以读写父窗口的URL,URL有一部分被称为frag,就是#号及其后面的字符,它一般用于浏览器锚点定位,Server端并不关心这部分,应该说HTTP请求过程中不会携带frag,所以这部分的修改不会产生HTTP请求,但是会产生浏览器历史记录。
示意图
FIM的原理就是改变URL的frag部分(hash)来进行双向通信。每个window通过改变其他window的location来发送消息,并通过监听自己的URL的变化来接收消息。这个方式的通信会造成一些不必要的浏览器历史记录,而且有些浏览器不支持onhashchange事件,需要轮询来获知URL的改变,最后,URL在浏览器下有长度限制,这个制约了每次传送的数据量。
此方式可实现iframe与父网页的双向通信。
实验
代码见source\fragment-identitier-messaging
window.name
与服务器通信 | iframe通信 |
× | √ |
window对象的name属性是一个很特别的属性,当该window的location变化,然后重新加载,它的name属性可以依然保持不变。
那么我们可以在页面A中用iframe加载其他域的页面B,而页面B中用JavaScript把需要传递的数据赋值给window.name,iframe加载完成之后,页面A修改iframe的地址,将其变成同域的一个地址,然后就可以读出window.name的值了。
这个方式非常适合单向的数据请求,而且协议简单、安全。不会像JSONP那样不做限制地执行外部脚本。
实验
代码见source\window-name
Flash 本地通信(Flash LocalConnection)
与服务器通信 | iframe通信 |
× | √(flash之间) |
页面上的双向通信也可以通过Flash来解决,Flash API中有LocalConnection这个类,该类允许两个SWF之间通过进程通信,这时SWF可以播放在独立的Flash Player或者AIR中,也可以嵌在HTML页面或者是PDF中。
遵循这个通信原则,我们可以在不同域的HTML页面各自嵌套一个SWF来达到相互传递数据的目的了。SWF通过LocalConnection交换数据是很快的,但是每次的数据量有40kb的大小限制。用这种方式来跨域通信过于复杂,而且需要了2个SWF文件,实用性不强。
window.postMessage
与服务器通信 | iframe通信 |
× | √ |
postMessage是html5为了解决跨域通信,特别引入的一个新的API,目前支持这个API的浏览器有:Firefox, IE8+, Opera, Safari, Chrome。postMessage允许页面中的多个iframe/window的通信,postMessage也可以实现ajax直接跨域,不通过服务器端代理。
1 2 | var message = 'hello,RIA之家! ' + (new Date().getTime()); window.parent.frames[1].postMessage(message, '*'); |
iframe1.html需要向iframe2.html发送消息,也就是第二个iframe,所以是window.parent.frames[1],如果是向父页面发送消息就是window.parent。
postMessage这个函数接收二个参数,缺一不可,第一个参数即你要发送的数据,第二个参数是非常重要,主要是出于安全的考虑,一般填写允许通信的域名,这里明河为了简化,所以使用’*’,即不对访问的域进行判断。
iframe2.html中写个监听message事件,当有消息传到iframe2.html时就会触发这个事件。
1 2 3 4 5 6 7 8 9 10 11 | var onmessage = function(e) { var data = e.data,p = document.createElement('p'); p.innerHTML = data; document.getElementById('display').appendChild(p); }; //监听postMessage消息事件 if (typeof window.addEventListener != 'undefined') { window.addEventListener('message', onmessage, false); } else if (typeof window.attachEvent != 'undefined') { window.attachEvent('onmessage', onmessage); } |
由于它是一个很新的方法,所以在较旧的浏览器中都无法使用。
实验
实验文件在source\window-postMessage
Cross Frame
与服务器通信 | iframe通信 |
× | √ |
Cross Frame是FIM的一个变种,它借助了一个空白的iframe,不会产生多余的浏览器历史记录,也不需要轮询URL的改变,在可用性和性能上都做了很大的改观。
示意图
它的基本原理大致是这样的,假设在域www.a.com上有页面A.html和一个空白代理页面proxyA.html, 另一个域www.b.com上有个页面B.html和一个空白代理页面proxyB.html,A.html需要向B.html中发送消息时,页面会创建一个隐藏的iframe, iframe的src指向proxyB.html并把message作为URL frag,由于B.html和proxyB.html是同域,所以在iframe加载完成之后,B.html可以获得iframe的URL,然后解析出message,并移除该iframe。当B.html需要向A.html发送消息时,原理一样。
Cross Frame是很好的双向通信方式,而且安全高效,但是它在Opera中无法使用,不过在Opera下面我们可以使用更简单的window.postMessage来代替。
实验
代码见source\cross-frame
server proxy
与服务器通信 | iframe通信 |
√ | × |
在数据提供方没有提供对JSONP协议或者window.name协议的支持,也没有对其它域开放访问权限时,我们可以通过server proxy的方式来抓取数据。
例如当www.a.com域下的页面需要请求www.b.com下的资源文件asset.txt时,直接发送一个指向www.b.com/asset.txt的ajax请求肯定是会被浏览器阻止。这时,我们在www.a.com下配一个代理,然后把ajax请求绑定到这个代理路径下,例如www.a.com/proxy/, 然后这个代理发送HTTP请求访问www.b.com下的asset.txt,跨域的HTTP请求是在服务器端进行的,客户端并没有产生跨域的ajax请求。
这个跨域方式不需要和目标资源签订协议,带有侵略性,另外需要注意的是实践中应该对这个代理实施一定程度的保护,比如限制他人使用或者使用频率。
各方法总结
方法 | 一句话说明 | 优点 | 缺点 |
CORS | 解决跨域问题的新的标准 | 原生方法 | 旧的浏览器不支持 |
JSONP | 创建script标签加载数据 | 兼容性好 | 需要callback |
Flash URLloader | 利用flash跨域加载数据 | 有flash就好用 | 没flash用不了 |
form跨域post | 利用form跨域post | 简单 | 只能单向发往服务器 |
hash | 利用hash实现跨域通信 | 可以实现iframe跨域通信 | 信息量少 |
window. name | 利用window.name实现跨域通信 | 可以实现iframe跨域通信 | 单向 |
flash本地通信 | 两个SWF之间通过进程通信 | 可以实现跨iframe、页面的跨域通信 | 复杂,没flash用不了 |
window. postMessage | 新的功能,可以解决跨域iframe通信 | 功能完整 | 旧的浏览器不支持 |
Cross Frame | 利用多个iframe进行双向跨域iframe通信 | 双向 | 复杂 |
server proxy | 通过服务器获取数据 | 数据源格式不限 | 需消耗服务器,有侵略性 |
示例代码们
淘宝UED已经有建立一个代码库,可以直接去看:https://github.com/colorhook/crossdomain
跨域中的安全问题
前端的安全问题最近愈加突出,所以安全问题需要第一考虑。
因为安全问题需要前后台共同关注,这里不细说,给出几个链接:
CSRF:http://en.wikipedia.org/wiki/Cross-site_request_forgery
XSS:http://en.wikipedia.org/wiki/Cross-site_scripting
参考
Cross-Origin Resource Sharing协议的介绍
http://yaoweibin2008.blog.163.com/blog/static/1103139201110942226377/
flash.net.URLLoader (ActionScript 3.0)
http://www.actionscript.com.cn/help/flash/net/URLLoader.html
利用HTML5的window.postMessage实现跨域通信
JavaScript的同源策略
https://developer.mozilla.org/Cn/JavaScript%E7%9A%84%E5%90%8C%E6%BA%90%E7%AD%96%E7%95%A5
XMLHttpRequest Level 2的简单例子
http://www.cnblogs.com/rubylouvre/archive/2010/05/27/1744889.html
使用 window.name 解决跨域问题
http://www.planabc.net/2008/09/01/window_name_transport/
前端跨域总结
http://www.slideshare.net/zhangsuoyong/ss-10511572
学习总结:前端跨域请求的解决办法——JSONP
http://lc87624.iteye.com/blog/1123148
也来谈谈”完美”跨域
JavaScript跨域问题分析与总结
http://www.uuxiao.com/diary_show.asp?id=632
XMLHttpRequest Level 2
http://www.w3.org/TR/XMLHttpRequest/
Cross-Origin Resource Sharing
跨域资源共享的10种方式–来源于淘宝UED
http://superchaowen.blog.163.com/blog/static/1658685452011722102827895/
跨来源资源共享
http://zh.wikipedia.org/wiki/%E8%B7%A8%E4%BE%86%E6%BA%90%E8%B3%87%E6%BA%90%E5%85%B1%E4%BA%AB
JSONP
http://zh.wikipedia.org/wiki/JSONP
利用form表单跨域post
http://hi.baidu.com/gguoyu/blog/item/08179d2340e76a489822edbb.html