web服务器与file服务器不同域(Domain)之间的数据交换
引言
前些天在对公司原有的 web 应用进行改版时遇到一个问题,当时需要从原有的应用中提取出一部分,用一个更为通用的来进行替换,并且仍然保留原有的应用接口。原有的应用属于 news.mycompany.com 域,而新应用将被部署到 upload.mycopany.com。当我试着从新的域向 news.mycompany.com 传递数据时,在前台遇到了浏览器返回的“拒绝访问(Access Denied)” 的错误信息,通过参考在 google 中查到的大量英文资料找到了问题的症结,并通过指定两个域中页面的 docment.domain 属性使问题得到了部分解决。后来一时兴起在 google 中查找与 document.domain 相关的中文资料,但得到的大部分是网络安全方面的文章,很少有文章提及通过指定页面的 document.domain 属性能够实现两个域之间的数据交换,于是决定写下此文,希望能够做到抛砖引玉吧。问题的提出
在开发 Web 应用时经常会遇到需要在两个帧之间传递数据的情况,这里的帧可以是 frameset 中的 frame 也可以是独立的窗口。常见的情况是一个帧作为应用的主体,另一个帧则提供一些供用户选择的选项,用户选择完毕后,该帧把用户作出的选择发送到服务器并向主要的帧传递一些信息,这里的信息可能是用户的选择也可能是服务器返回的数据。当两个帧中的内容同属于一个域时实现以来比较简单,但是当它们分属于不同域时问题就变得复杂而棘手了,因为这里涉及到了数据访问的安全性问题,搞不好就会遇到浏览器返回的“拒绝访问(Access Denied)” 的错误信息。可能的解决方案
下面我们将通过几个试验来分析一下在分属于不同域的帧之间传递数据的一些方法。- 利用客户端脚本(如 JavaScript)和窗口句柄在两个帧之间传递数据
- 利用 MSIE 提供的对话框在两个帧之间传递数据
- 利用服务器端的应用,通过 session 来传递数据
方案一
用客户端脚本实现两个帧之间的数据交换应该是最为轻量级的方式之一了,这样做不会增加服务器的负载也不会占用网络带宽,数据交换完全是在客户端完成。下面就让我们先来了解一下用客户端脚本(以 JavaScript 为例)和窗口句柄如何实现一个域内的数据交换。我们通过一个实例来进行说明:假设需要给用户提供一个新闻的录入界面,用户可以用它录入新闻的原始内容,并且可以在其中嵌入一副图片。为了实现这个功能界面我们设计了两个帧,或者说是两个窗口:
- 主窗口: 新闻内容的主要编辑界面,用户可以在里面录入新闻的标题、作者、新闻主体等内容,还有一个图片框可以预览上传的图片
- 弹出窗口: 处理图片上传的界面,用户可以选择本地图片进行上传,成功后它把服务器上文件的 url 返回给主窗口进行预览
其中 NewsEdit.html 位于 news.mycompany.com 的主目录下,其源代码如下所示:
<!-- File: NewsEdit.html (http://news.mycompany.com/NewsEdit.html) --> <html> <head> <title>The Content Editing Interface</title> <meta http-equiv="Content-Type" content="text/html; charset=gb2312"> <script language="JavaScript"> <!-- /* OpenWin 用来在一个弹出窗口中显示 ImgUpload.html 的内容*/ function OpenWin(){//Open window url='http://news.mycompany.com/upload/ImgUpload.html'; newwindow = window.open(url,"ImgUpload","height=135,width=300"); if (!newwindow.opener) newwindow.opener=self; } --> </script> </head> <body> <h2>Edit your content here</h2> <!-- 调用后台应用 newsedit 来保存新闻内容 --> <form action="http://news.mycompany.com/newsedit" method="post" name="addnews"> <!-- 新闻标题 --> Title:<input type="text" name="title"><br> <!-- 新闻作者 --> Author:<input type="text" name="author"><br> The content <br> <!-- 新闻内容 --> <textarea name="contentBody" cols="100" rows="10"></textarea> <br> <!-- 点击连接打开上传图片的小窗口 --> <a href="JavaScript:OpenWin()">Upload Image File</a> <br> <!-- UserImg 用来预览上传成功后的图片文件 --> <img name="UserImg" style="width: 100px; height: 100px;" src="" border="1"> <br><br> <input type="submit" name="SaveContent" value="Submit"> <input type="reset" name="ClearContent" value="Reset"> </form> </html> |
ImgUpload.html 位于 news.mycompany.com 的 upload 子目录下,其源代码如下所示:
<!-- File: ImgUpload.html (http://news.mycompany.com/upload/ImgUpload.html) --> <html> <head> <title>Imgage Upload Interface</title> <meta http-equiv="Content-Type" content="text/html; charset=gb2312"> </head> <body> <h2>Image Upload</h2> <!-- 调用后台应用来处理上传的图片 --> <form action="http://news.mycompany.com/upload/imgupload" method="post" enctype="multipart/form-data" name="upload"> <!-- 由用户选择本地文件 --> <input type="file" name="imgfile"> <input type="submit" name="Submit" value="Upload"> </form> </html> |
另外介绍一下我们的两个后台应用:
- newsedit: 位于 news.mycompany.com 的主目录下,接受用户的 POST 请求,将编辑界面的新闻元素存储到后台数据库
- imgupload: 位于 news.mycompany.com 的 upload 子目录下,接受用户的 POST 请求,将本地的图片文件上传到服务器,并返回图片文件完整的 url。
<html> <head> <title>File Upload Successfully</title> </head> <body> <h3>File Uploaded Successfully!</h3> <script language="JavaScript"> <!-- 获取主窗口的句柄 --> parwin=self.opener; <!-- 获取对 img 元素的引用,并用上传文件的 url 为 img 元素的 src 属性赋值,这样在客户端就可以预览了 --> <!-- 为了简化问题,我们将对 img 元素的引用直接写在程序中 --> parwin.addnews.UserImg.src="http://news.mycompany.com/img/2003_07/06/1057478464859.gif"; </script> </body> </html> |
返回的页面除了显示成功信息外,还利用脚本向主窗口传递了上传文件的 url,具体过程是:
首先通过 "self.opener" 获得主窗口(即 NewsEdit.html 所在窗口)的句柄;然后用上传文件的 url 对主窗口中 UserImg 元素的 src 属性进行赋值,这样在主窗口中就可以看到上传后的图片了。
好了,我们的第一个实验已经成功了,实验结果告诉我们:当两个帧中的内容同属于一个域时,利用客户端脚本和窗口句柄在其中传递数据是没有问题的。接下来我们把 ImgUpload.html 和 imgupload 从 news.mycompany.com 提取出来,部署到 img.mycompany.com 的对应目录下,并修改 NewsEdit.html 中引用 ImgUpload.html 时的 url。这样当我们试着用 JavaScript 从 img.yourcompanu.com 向 bbs.yourcompany.com 传递数据时,浏览器就会弹出“拒绝访问(Access Denied)” 的错误框,表明我们违反了他的安全策略,并且数据无法正常传递过来。
其实,你可以直接把实验一中 imgupload 返回的内容另存为一个文件并部署到 img.mycompany.com,在 NewsEdit.html 中调用 window.open() 方法直接引用这个文件就可以进行测试了。 |
我们之所以会遇到“拒绝访问(Access Denied)” 的的错误信息,其原因在于:
最初,浏览器的开发商、开发团体出于安全性的考虑,默认情况下是不允许在分属于不同域的页面之间进行数据交换和方法调用的,当遇到这种情况时浏览器就会返回“拒绝访问(Access Denied)”的错误。
“那为什么即使我的两个页面属于同一个域我还是会遇到‘拒绝访问’的错误呀?”
如果是这种情况,那就要看你的弹出窗口中的内容是否始终属于同一个域,看一下你的 ImgUpload.html 是不是调用了属于其他域的应用,并且该应用在窗口中重新写入了内容,如果是这样那你的弹出窗口就变质了,它最后属于另外一个域,你当然会遇到“拒绝访问”的错误。
“这么说如果两个页面分属于不同域的话我们就无法在两个窗口之间传递数据了吗?”
事实基本上是这样的--一个令人沮丧的消息。
但答案也并非绝的--好像还有希望。
是的,一些浏览器的开发商、开发团体在开发高版本的浏览器时对原有策略进行了部分调整,这些调整给我们带来了一线生机:
当两个页面在进行数据交换时,浏览器会首先比较两个页面的 domain 属性,如果 domain 属性相同,那么浏览器就允许它们之间的数据交换,否则就返回“拒绝访问(Access Denied)”的错误。
“那么我们如何才能蒙蔽浏览器,让它认为两个页面的 domain 属性相同呢?”
这就要靠脚本来实现了,在 JavaScript 中我们可以通过在页面中加入如下声明来强制指定页面所属的域。
<script language="JavaScript">
<!--
document.domain = "mycompany.com"; //指定 document 所属的域
-->
</script>
加入上面的声明就可以蒙蔽浏览器,在原本属于两个不同域的页面之间进行数据交换了。但需要注意:只有把上面的声明加入到需要进行数据交换的所有文件中才会有效,只在一个域的文件中加入上面的声明是不起作用的。另外,声明部分最好能插入到页面的 <head></head> 标记中间,这一点也是用脚本进行开发时所被提倡的。有关 JavaScript 中的 document 和 domain 等可以参考 http://www.werelight.com/docs/JavaScript_Quick_Reference.htm
“使用这种方法有什么限制码?”
因该说用这种方法来实现不同域之间的数据传递还是有很多的限制的,主要表现为以下两点:
- document.domain 属性是不可以随便设置的,它只能被设置为文件所属域的上级域。如假设 ImgUpload.html 属于 img.mycompany.com ,那么它的 document.domain 属性可以设置为“mycompany.com” ,但不能设置为“img.mycompany” 或其他的,如“foo.com”。
- 只有当两个域存在相同的上级域时,才能通过指定 document.domain 来实现它们之间的数据交换,并且 document.domain 属性必须被 设置为二者的公共域。例如,假设 NewsEdit.html 属于 news.mycompany.com,而 ImgUpload.html 属于 img.yourcompany.com,那么无论你如何设置 document.domain 都无法在它们之间交换数据;再比如,假设 ImgUpload.html 属于 img.mycompany.com,那么我们可以把两个页面的 document.domain 属性设置为“mycompany.com”,但不可以设置为“img.mycompany” 或其他的什么域,如“foo.com”。
- 并非所有的浏览器都支持对 document.domain 属性进行设置。如 MSIE 和 Netscape 它们4.0以前的版本是不支持对该属性的设置的;另外有趣的是虽然 Netscape 在4.0以后开始支持对 domain 进行设置了,但在4.03 和4.04两个版本中 Netscapre 居然又把上面的功能给取消了。
利用其他脚本,如 vbscript 或 jscript 实现这种跨域的数据交换其原理与用 JavaScript 是一样的,大家可以参考相关资料来实现。 |
方案二
下面我们来看一下利用 MSIE 提供的对话框能不能解决两个域之间的数据交换问题首先我来简单介绍一下 MSIE 对话框:MSIE 提供的 showModalDialog 和 showModelessDialog 方法可以用来在一个单独的帧中显示一个模态或非模态对话框,两个方法都通过一个 URI 参数来指定对话框帧中的内容;可选的参数 vArguments 用来向对话框帧传递任何类型(包括数组类型)的参数;另外还有一个可选的参数 sFeatures 是用来定义对话框帧的显示效果,如位置、字体等等的;
注意 Netscape Navigator 、Mozilla 和 Opera 浏览器是没有与之对应的方法的,也就是说除了 MSIE 之外其他几大浏览器都不支持用 showModalDialog 或 showModelessDialog 显示对话框,如果你感兴趣的话这里有一篇文章能够教你如何通过其他方式来模拟一个模态对话框,详见 Simulating Modal Dialog Windows |
“Because a modal dialog box can include a URL to a resource in a different domain, do not pass information through the vArguments parameter that the user might consider private. The vArguments parameter can be referenced within the modal dialog box using the dialogArguments property of the window object.”--引自 MSDN showModalDialog
上面一段话说明:通过 sURL 参数我们可以将另一个域的资源用为话框的内容,但这种情况下我们就不能再向对话框传递任何参数了,只有当所引用的资源与引用它的页面属于同一个域时,我们才可以利用 window.dialogArguments 获得从引用页中传递过来的参数。
“那么我能不能像方案一中那样通过强制指定两个页面中的 document.domain 属性来蒙蔽浏览器,使其认为两个页面属于同一个域呢?”
确实有人提出过这种想法,笔者也试着这样做过,但最后还以失败而告终:在两个页面中强制指定 document.domain 了属性后,无论两个页面是否属于同一个域,对话框都无法正常识别从主页面传递过来的参数。
在此次实验中我使用了3个文件
- main.html : 部署在 a.mycompany.com,通过调用 showModalDialog 引用另外两个文件
- localdialog.html : 与 main.html 一起部署在 a.mycompany.com
- remotedialog.html : 部署在 b.mycompany.com,其内容与 localdialog.html 完全一样
其中 main.html 的源代码如下所示:
<html> <head> <title>show modal dialog</title> <script> <!-- //document.domain = "mycompany.com"; <!-- 打开一个模态对话框,显示 url 所代表的资源 --> function openDialog(url) { <!-- 向对话框传递参数 --> var args = new Object(); args.content = "Can you hear me?"; var rv = window.showModalDialog(url, args); <!-- 显示对话框所返回的结果 --> if (rv) { alert("dialog returns :" + rv); } else { alert("dialog returns nothing"); } } --> </script> </head> <body> <!-- 引用 b.mycompany.com 中的资源 --> <a href="#" onclick="openDialog('http://b.mycompany.com/remotedialog.html');return false;"> I will Open a remote dialog from news.soufun.com </a> <br> <!-- 引用本地的资源 --> <a href="#" onclick="openDialog('./invokebyhouse.html');return false;"> I will Open a local dialog </a> </body> </html> |
localdialog.html(remotedialog.html) 的源代码如下所示:
<html> <head> <title>a remote dialog</title> <script> <!-- //document.domain = "mycompany.com"; onload = function() { var args = window.dialogArguments; alert("You send me: " + args.content); btnCan.onclick = function() { window.returnValue = "Yes I do, I hear you from " + document.domain; close(); } } --> </script> </head> <body> Im here, Im a dialog <br> I will return something to the main window<br> <input id="btnCan" type="button" style="text-align:center;" value="Close"> </body> </html> |
通过实验发现:
- main.html 总是能正常的接收从对话框中返回的结果,无论对话框是位于a.mycompany.com 还是 b.mycompany.com,也无论是否设置了 document.domain 属性;
- 在没有设置 document.domain 属性时,localdialog.html 可以正常接收从 main.html 传递过来的参数,但如果设置了 document.domain 属性, localdialog.html 读取到的参数就变成 null 了。
- 而无论是否设置了 document.domain 属性,在 remotedialog.html 读取从 main.html 传递过来的参数得到的始终都是 null。
如果我的实验中存在某些漏洞,或者在你的实验中对话框读取到了从 main.html 传递过来的参数,有劳你通过 Email告知我,谢谢! 注(2004-12-28):您在进行测试的时候可能会得到不同的测试结果,因为随着IE 的更新或者补丁的作用,这种跨域的数据交换行为可能会被调整。例如笔者在加入这段注解的时候上面的第1条就已经不再成立了——main.html 并不是总能接收到从隶属于b.mycompany.com 的对话框返回的数据,只有两者都把document.domain 属性设置为mycompany.com 后main.html 才能接收到对话框返回的结果。见相关讨论:about showModalDialog |
方案三
应该说利用服务器端应用实现这种跨域的数据交换是最为可靠的方式了,因为这几乎不会受到客户端的限制,不像前面两种方式:有的客户端不支持 document.domain 属性,有的不支持对话框等等。那么是不是说用服务器端应用解决不同域之间的数据交换是应该最优先考虑的方案呢?答案是否定的,因为服务器端应用也有它的致命伤:即接收数据的一方不能实时的显示从对方传递过来的数据,它只有在处理了 GET 或 POST 请求后才能使数据得到展现,在这个过程中如果未做任何特殊处理,那么用户在该帧中编辑的内容将被清除掉,这往往是我们所不希望看到的。有关用服务器端应用实现跨域的数据交换我们就不再举例子了,如果您有兴趣的话可以到 google 上查一下相关的资料。总结
在上面介绍的三种方案中,除方案二尚不能实现在分属于不同域的帧之间进行数据交换之外,经证明方案一和方案三都是可行的,不过这两种方案又各有利弊:- 方案一的优点在于:用客户端脚本和窗口句柄不必占用服务器资源和网络带宽,可以做到数据的实时展现,并且不会影响到对方帧中的已有内容,其缺点是应用范围较小,要受到客户端浏览器的限制;
- 方案三由于是利用的服务器端应用,所以几乎不会受到客户端的影响,其缺点是无法做到数据的实时展现,有时候还要采取某种措施来维持对方帧中的已有内容。