利用客户端解决方案改进跨域通信

逐步解决 SOP 限制

Ajax 在 Web 2.0 时代起着非常重要的作用,然而有时因为同源策略 (SOP) 它的作用会受到限制。在本文中,将学习如何克服合作限制。文中将探讨与其他服务器和供应商进行集成的一些客户端解决方案。这些示例将向您展示如何改进您的跨域通信和数据传输。

Wang Jiaye, 软件工程师, IBM

Wang Jiaye 是一名软件工程师,是 IBM 中国开发实验室的上海本地化团队的一名成员。她曾在全球化团队工作过,从事过 UT/ClickTrack(Web 分析工具)方面的工作。Wang 对新的 Web 技术非常感兴趣。



Hu Changchun, 软件工程师, IBM

Hu Changchun 是上海 IBM 中国开发实验室的一名软件工程师。他从事 J2EE 和 Web 技术方面的工作。



2012 年 6 月 28 日

简介

越来越多的网站需要相互协作。例如,在线房屋租赁网站需要谷歌地图的支持,以显示特定租赁房屋的位置。为了满足这样的需求,已经出现了各种各样的 mashup。mashup 是一种将不同供应商的数据或组件集成起来,使之更加有用或更用户化的 Web 应用程序。Mashup(或协作功能)被认为是 Web 2.0 的重要组成部分。

出人意料的是,将异步的 JavaScript、XML (Ajax) 和 mashup 结合起来并不容易。由于浏览器施加的安全限制,让页面上的不同小部件彼此之间相互通信比较麻烦。通常,您可以通过在服务器端设置一个代理来解决此问题,该方法是不可扩展的。本文中,将了解客户端上针对跨域通信和数据传输的其他一些解决方案。

安全限制

同源策略 (SOP) 可防止从一个来源加载的脚本获取或操纵来自另一来源的文档的属性或方法。术语来源 是域名、应用程序协议和运行脚本文档的端口的结合。可能存在关于 SOP 概念的一些误解;SOP 指只能从站点 A、不能从站点 B 获取信息。您需要知道在 SOP 限制下可以做什么,不可以做什么。

SOP 限制

例如,来自源点 A 的 Web 页面可以:

  • 从源点 B 获取脚本、CSS 样式表或图像
  • 包含一个指向源点 B 中页面的 iframe/frame。
  • 使用 HTML 元素的 src 属性(比如iframeimg)将信息发送到源点 B 。

来自源点 A 的 Web 页面不可以:

  • 对源点 B 进行 Ajax 调用
  • 读取或操纵 iframe/frame 中指向页面 B 的内容

为什么会这样做呢?主要是为了保护用户的重要信息。假设如下:如果某个用户与供应商交互,并且不希望提交到站点的任何信息被泄露到其他站点。这种约束限制了不同站点之间的协作,但可以保护用户免受潜在的恶意攻击。

解决这个问题有很多方法。例如,JSONP 利用了 Web 页面可以从任何源码中下载脚本的能力。但 JSONP 有两个主要的限制:它与 Ajax 调用一样没有错误处理机制,并且脚本标记请求要使用 Get method,其中对长度有所限制。(有关 JSONP 的更多信息,请参阅 参考资料 部分)。

以下各小节探讨了针对跨域通信和数据传输的客户端解决方案。每个解决方案都有其优点和缺点;应用程序场景在很大程度上会影响您的选择。


跨子域的解决方案

如果源点 A 和 B 共享相同的超级域,那么更改 document.domain 属性可以使两个文档轻松进行互访。在 HTML 规范中,document.domain 是只读属性;目前的大多数浏览器允许将它设置成超级域(而不是顶级域)。例如,一个 URL 为 www.myapp.com/index.html 的文档可以将其域名设置为 myapp.com,而另一个来自 sample.myapp.com/index2.html 的文档也可以将域名设置为 myapp.com。图 1 显示了document.domain 的作用方式。

图 1. document.domain
具有来自相同超级域的不同 URL 的文档通过将其各自的域设置为同一超级域来实现通信的描述

利用跨子域的解决方案,不同子域的源点可以在相同超级域下进行通信,这不是 SOP 限制。但是,严格地说,跨子域的解决方案最适合 intranet 应用程序。


URL.hash(fragment id) 解决方案

一个 URL 由几部分组成,如下图 2 所示:

图 2. URL 组件
带下划线的 URL 的屏幕截图,并指出了 URL 不同组成部分:协议、主机、路径、查询和 hash

通常,URL 的任何更改都会导致加载新的页面。唯一的例外是 hash 值的变化。任何 URL 的 hash 更改都不会导致页面刷新。许多 Web 2.0 网站中都已经开始广泛使用 Hash,以标记部分刷新页面时的每一个步骤。在跨域通信中,hash 是一笔宝贵的资产。虽然在获取对方 hash 值方面有一些限制,但来自不同源点的文件可以设置对方的 URL,包括 hash 值。文档相互之间可以使用 hash 传送信息。图 3 显示了一个示例。

图 3. 使用 URL.hash(fragment id) 进行通信
具有不同 URL 的文档通过更改其他文档的 hash 值并监视自身 hash 的变化来进行通信的描述

在图 3 中,当 A 想要向 B 发送信息时,它可以修改 B 的 hash 值,如清单 1 所示:

清单 1. 通过 url.hash 发送信息
function sendMsg(originURL, msg){
	var data = {from:originURL, msg:msg};
	var src = originURL + “#” + dojo.toJson(data);
	document.getElementById('domainB').src=src;
}

B 中的函数将轮询自身的 hash 值,找出 A 发送的是什么,然后它可以用同样的方式回复 A。如果 A 想接收这条消息,也需要轮询其本身的 hash 值。

清单 2. 监视 url.hash 并从中接收信息
window.oldHash="";
checkMessage = function(){
	var newHash = window.location.hash;
	if(newHash.length > 1){
		newHash = newHash.substring(1,newHash.length);
		if(newHash != oldHash){
 		oldHash = newHash;
 		var msgs = dojo.fromJson(newHash);
 		var origin = msgs.from;
 		var msg = msgs.msg;
 			 sendMessage(origin, "Hello document A");
 		 }
 	}
}
window.setInterval(checkMessage, 1000);
sendMessage = function(target, msg){
	var hash = "msg="+ msg;
	parent.location.href= target + “#” + hash;
}

像 JSONP 一样,这种方法也有一个长度限制,但它可以更好地处理错误。一些特殊字符,比如问号(?),是 URL 中的保留字符,应先对这些字符进行编码。

清单 3. 通过 url.hash 发送包含特殊符号的信息
function sendMsg(originURL, msg){
	…
	var src = originURL + “#” + encodeURI (dojo.toJson(data));
	…
}

接收时,应先进行解码。

清单 4. Receiving message containing special characters
function checkMsg(){
	…
	var msgs = decodeURI(dojo.fromJson(newHash)); 
	…
}

Cross-fragment 技术

由于许多网站将 hash 用于其他用途,对于这些网站来说,要将 URL.hash(fragment id) 技术并入跨域通信和数据传输是比较复杂的。跨帧 (Cross-frame) 信息传递有点类似片段 id 信息传递。图 4 显示了跨段 (cross-fragment) 是如何起作用的。

图 4. 跨段技术
具有不同 URL 的文档通过自动创建指向相同域代理的 iframe 作为目标文档进行通信的描述,iframe 可以通过调用目标文件中的相应功能来获取请求/数据和响应

在上图中,当 A 想要与 iframe B 进行通信时,会先在其自身中创建一个 iframe。这个 iframe 指向与 B 位于同一域中的 “代理” C。代理 URL 中包含参数、数据以及 B 的框架标识符。

清单 5. 使用跨段技术发送消息
function sendMsg(msg){
 var frame = document.createElement(“iframe”);
 var baseProxy = “http://www.otherapp.com/proxy.html”;
 var request = {frameName:’otherApp’,data:msg};
 frame.src = baseProxy+”#”+encodeURI (dojo.toJson(request));
 frame.style.display=”none”;
 document.body.appendChild(frame);
}

当 C 加载信息时,它会从 A 中获取请求和数据并调用 B 中相应的方法。由于 B 和 C 在相同的域中,所以可以通过另一个窗口的处理程序直接调用对方的方法。A 可以成功地向 B 发送信息,B 也可以用同样的方式进行响应。

清单 6. 使用 cross-fragment 技术接收消息
window.onLoad = function(){
     var hash = window.location.hash;
     if(hash && hash.length>1){
          var request = hash.substring(1,hash.length);
          var obj = dojo.fromJson(decodeURI (request));
          var data = obj.data;
          //process data
          parent.frames[obj.frameName].getData(…);// getData in a function defined in B
     }
}

OpenAjax 实现

OpenAjax 提供了托管集线器模块,以支持基于 fragment id 和 cross-frame 技术的跨域通信和数据传输。托管集线器模块包括管理器端和客户端。托管集线器中包含一个消息集线器,用于存储消息。每一个想要与其他部件通信的小部件都需要在自身建立一个集线器客户端,并且还需建立一个相应的容器来连接它。该容器将与代表客户端的托管集线器进行交互。客户端可以利用订阅/发布机制发送和接收消息。OpenAjax 的基本工作流如图 5 所示。

图 5. OpenAjax 的主要工作流
OpenAjax 用来实现跨域通信的主要工作流的描述

Window.name 解决方案

Window.name 是跨域通信和数据传输的有点棘手的一个解决方案。通常,window.name 的使用方式如下。

  • 使用 window.frames[windowName] 获取子窗口。
  • 将它设置成链接元素中的目标属性。

虽然 window.name 的显著特点是非常适合将它用作不同源点文档之间的 “桥”,但它不是一个常用属性。无论加载什么页面,window.name 的值都不会发生改变。在 SOP 限制下如何使用它?图 6 说明了 window.name 是如何协助跨域通信的。

图 6. window.name 和 cross-domain 通信
通过使用 window.name 存储服务器响应、使用客户端回滚并检查 window.name 值来获取来自其他域的资源的描述

页面 A 想获取其他源点的资源或 Web 服务时,它可以在自身中添加一个针对外部资源或服务的新的 iframe B。服务器会使用 HTML 文件进行响应,因此要设置数据的window.name 属性。由于现在 A 和 B 不在相同的域中,所以 A 仍然不能获取 B 的名称属性。B 获取数据后,应该可以导航回到任何与 A 位于相同域中的页面,使 A 可以访问名称属性。在 A 获取数据后,可以随时销毁 B。

将 dojox.io.windowName 用于跨域通信

Dojo 支持基于 window.name 的跨域通信。唯一的 API 是 dojox.io.windowName.send(method, args),它类似于 dojo.xhrGet/dojo.xhrPost。该方法可以是 GETPOST,并且 argsdojo.xhr 中的类似。例如,您可以在客户端发送一个跨域请求,如清单 7 所示:

清单 7. 通过 window.name 发送消息
var args = {
 url: "http://www.sample.com/testServlet?windowName=true",
 load: function(data){
 alert("You've got the data from server " + data);
    },
error: function(error){
 alert("Error occurred: " + error);
 }
}
dojox.io.windowName.send("GET",args);

可以按照使用 dojo.xhr 的方式来使用 dojox.io.windowName。对于服务器端,如果您希望利用 windowname 传输来访问资源或服务,建议您检查一下请求中的 windowName 参数。如果请求中包含此参数,那么服务器会用一个 HTML 文档进行响应,此文档将 window.name 设置为需要交付给客户端的数据。清单 8 中的代码显示了一个示例。

清单 8. window.name message 技术的后端支持
testServlet.java:
protected void doGet(HttpServletRequest request,HttpServletResponse response){
 //process request
 String returnData = ...;
 String isWindowNameReq = request.getParameter(“windowName”);
 if(null !=isWindowNameReq && Boolean.parseBoolean(isWindowNameReq)){
	 returnData = getCrossDomainStr(returnData);
}
 response.getOutputStream().print(returnData);
}
private String getCrossDomainStr(String data){
 StringBuffer returnStr = new StringBuffer();
 returnStr.append("<html><head><script type=\"text/javascript\">window.name='");
 returnStr.append(data);
 returnStr.append("'</script></head><body></body></html>");
 return returnStr.toString();
}

将框架导航回来源域的任何页面时,您需要保证域中的这个页面是存在的。如果页面不存在,Internet Explorer 将会出现问题。在 Firefox 中,您只需要使用 blank.html。在 Dojo 中,您可以指定使用 dojo.dojoBlankHtmlUrl 属性时的回滚页面。默认情况下,将该页面设置为 Dojo 库中的 dojo/resources/blank.html。

使用 window.name 传输的数据量远远多于使用 url.hash 传输的数据。目前的大多数浏览器都支持基于 window.name 的 16M+ 数据转换。


HTML5 中的新功能

在 HTML5 规范的草案中,新方法 window.postMessage(message, targetOrigin) 可用于安全跨域通信。调用该方法时,会分派一个消息事件,如果该窗口正在监听消息事件,那么它能够从事件中获取消息以及消息来源。图 7 显示了一个示例。

图 7. 使用 HTML5 进行跨域通信
对使用 HTML5 中的固有功能 (window.postMessage) 和信息事件监听器进行跨域通信的描述

在图 7 中,当 iframe 想要通知来自另一个源点的父窗口已成功完成加载时,它可以通过 window.postMessage 发送消息。同时,它将监测反馈消息,如清单 9 所示:

清单 9. 通过 HTML5 的新方法发送消息
http://www.otherapp.com/index.html
function postMessage(msg){
     var targetWindow = parent.window;
      targetWindow.postMessage(msg,"*");
}
function handleReceive(msg){
 var object = dojo.fromJson(msg);
 if(object.status == “ok”){
	//continue to do other things
	……
 }else{
	//retry sending msg
	……
 }
}
window.addEventListener("message", handleReceive, false);
window.onLoad = function(){
    postMessage("already loaded");
}

父文档将监听消息事件。当消息到达时,会先检查它是否来自 www.otherapp.com,然后返回确认消息。

清单 10. 通过 HTML5 的新方法接收消息
http://www.myapp.com/index.html
function handleReceive(event){ 
    if(event.origin != "http://www.otherapp.com")
        return; 
     //process data
     ……
     var otherAppFrame = document.getElementById(“otherApp”) 
     otherAppFrame.postMessage(“{status:’ok’}”,”http://www.otherapp.com”);
}
window.addEventListener("message", handleReceive, false);

清单 10 中的示例代码可以在 Firefox 3+、Internet Explorer 8、Google Chrome 2、Opera 9+ 以及 Safari 4 中运行。它使得来自不同来源的文档之间的通信变得更加方便。此外,如果不希望自己的文档收到来自其他文档的任何消息,那么不要添加事件监听器并删除所有消息。

参考资料

学习

获得产品和技术

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=823281
ArticleTitle=利用客户端解决方案改进跨域通信
publish-date=06282012