吕克让的资料库 - lhelper's repository

克己服人,礼智谦让!
Weblcome to lhelper's repository!

Thursday, June 10, 2004

 

实现不同域(Domain)之间的数据交换

引言


前些天在对公司原有的 web 应用进行改版时遇到一个问题,当时需要从原有的应用中提取出一部分,用一个更为通用的来进行替换,并且仍然保留原有的应用接口。原有的应用属于 news.mycompany.com 域,而新应用将被部署到 upload.mycopany.com。当我试着从新的域向 news.mycompany.com传递数据时,在前台遇到了浏览器返回的“拒绝访问(Access Denied)” 的错误信息,通过参考在 google 中查到的大量英文资料找到了问题的症结,并通过指定两个域中页面的 docment.domain 属性使问题得到了部分解决。后来一时兴起在google 中查找与 document.domain 相关的中文资料,但得到的大部分是网络安全方面的文章,很少有文章提及通过指定页面的document.domain 属性能够实现两个域之间的数据交换,于是决定写下此文,希望能够做到抛砖引玉吧。

关键字: JavaScript, Domain, Access Denied
日期:2003-07-06

问题的提出


在开发 Web 应用时经常会遇到需要在两个帧之间传递数据的情况,这里的帧可以是 frameset 中的 frame也可以是独立的窗口。常见的情况是一个帧作为应用的主体,另一个帧则提供一些供用户选择的选项,用户选择完毕后,该帧把用户作出的选择发送到服务器并向主要的帧传递一些信息,这里的信息可能是用户的选择也可能是服务器返回的数据。当两个帧中的内容同属于一个域时实现以来比较简单,但是当它们分属于不同域时问题就变得复杂而棘手了,因为这里涉及到了数据访问的安全性问题,搞不好就会遇到浏览器返回的“拒绝访问(Access Denied)” 的错误信息。

可能的解决方案


下面我们将通过几个试验来分析一下在分属于不同域的帧之间传递数据的一些方法。

方案一


用客户端脚本实现两个帧之间的数据交换应该是最为轻量级的方式之一了,这样做不会增加服务器的负载也不会占用网络带宽,数据交换完全是在客户端完成。下面就让我们先来了解一下用客户端脚本(以JavaScript 为例)和窗口句柄如何实现一个域内的数据交换。

我们通过一个实例来进行说明:假设需要给用户提供一个新闻的录入界面,用户可以用它录入新闻的原始内容,并且可以在其中嵌入一副图片。为了实现这个功能界面我们设计了两个帧,或者说是两个窗口:

为了简单起见,我们假设两个窗口中的内容都是静态的,主窗口对应的文件为 NewsEdit.html,弹出窗口对应的文件为 ImgUpload.html(而大多数情况下两个窗口的内容都应该是动态生成的)。

其中 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=" style="color: rgb(255, 0, 0);">title"><br>
  <!-- 新闻作者 -->
  Author:<input type="text" name=" style="color: rgb(255, 0, 0);">author"><br>
  The content <br>
  <!-- 新闻内容 -->
  <textarea name="contentBody"
cols="100" rows="10"></textarea>
  <br>
  <!-- 点击连接打开上传图片的小窗口 -->
  <a href="JavaScript:OpenWin( style="color: rgb(255, 0, 0);">)">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=" style="color: rgb(255, 0, 0);">upload">
  <!-- 由用户选择本地文件 -->
  <input type="file" name="imgfile">
  <input type="submit" name="Submit" value="Upload">
</form>
</html>


另外介绍一下我们的两个后台应用:

下面是 imgupload 处理完 POST 请求后返回的页面内容,该内容显示在 ImgUpload.html 所占据的弹出窗口中:






<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=" style="color: rgb(255, 0, 0);">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)” 的错误框,表明我们违反了他的安全策略,并且数据无法正常传递过来。

style="text-align: left; width: 100%; background-color: rgb(204, 204, 204);">





其实,你可以直接把实验一中 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


“使用这种方法有什么限制码?”
因该说用这种方法来实现不同域之间的数据传递还是有很多的限制的,主要表现为以下两点:







利用其他脚本,如 vbscript 或 jscript
实现这种跨域的数据交换其原理与用 JavaScript 是一样的,大家可以参考相关资料来实现。

方案二


下面我们来看一下利用 MSIE 提供的对话框能不能解决两个域之间的数据交换问题
首先我来简单介绍一下 MSIE 对话框:MSIE 提供的 showModalDialog
showModelessDialog
方法可以用来在一个单独的帧中显示一个模态或非模态对话框,两个方法都通过一个 URI 参数来指定对话框帧中的内容;可选的参数 vArguments
用来向对话框帧传递任何类型(包括数组类型)的参数;另外还有一个可选的参数 sFeatures 是用来定义对话框帧的显示效果,如位置、字体等等的;







注意 Netscape Navigator 、Mozilla 和
Opera 浏览器是没有与之对应的方法的,也就是说除了 MSIE 之外其他几大浏览器都不支持用 showModalDialog 或
showModelessDialog 显示对话框,如果你感兴趣的话这里有一篇文章能够教你如何通过其他方式来模拟一个模态对话框,详见 href="http://developer.netscape.com/viewsource/goodman_modal/goodman_modal.html">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 在调用用 showModalDialog 方法时,通过 vArguments 向对话框传递了参数:"Can you
hear me?",希望对话框能够接收到这个参数;如果对话框接收到了,那么它将调用 window.alert() 方法打印出这条消息,然后向
main.html 返回一个结果:"Yes I do, I hear you from " + document.domain ;如果
main.html 接收到了对话框返回的结果,那么它同样会调用 window.alert() 打印出结果的内容。

其中 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>


通过实验发现:

  1. main.html 总是能正常的接收从对话框中返回的结果,无论对话框是位于a.mycompany.com 还是 b.mycompany.com,也无论是否设置了 document.domain 属性;

  2. 在没有设置 document.domain 属性时,localdialog.html 可以正常接收从 main.html 传递过来的参数,但如果设置了 document.domain 属性, localdialog.html 读取到的参数就变成 null 了。

  3. 而无论是否设置了 document.domain 属性,在 remotedialog.html 读取从 main.html 传递过来的参数得到的始终都是 null。


非常遗憾,实验结果告诉我们:用对话框是无法实现这种跨域的数据交换的。

style="text-align: left; width: 100%; background-color: rgb(204, 204, 204);">





如果我的实验中存在某些漏洞,或者在你的实验中对话框读取到了从 main.html 传递过来的参数,有劳你通过 Email告知我,谢谢!

方案三


应该说利用服务器端应用实现这种跨域的数据交换是最为可靠的方式了,因为这几乎不会受到客户端的限制,不像前面两种方式:有的客户端不支持 document.domain 属性,有的不支持对话框等等。那么是不是说用服务器端应用解决不同域之间的数据交换是应该最优先考虑的方案呢?答案是否定的,因为服务器端应用也有它的致命伤:即接收数据的一方不能实时的显示从对方传递过来的数据,它只有在处理了 GET 或 POST 请求后才能使数据得到展现,在这个过程中如果未做任何特殊处理,那么用户在该帧中编辑的内容将被清除掉,这往往是我们所不希望看到的。有关用服务器端应用实现跨域的数据交换我们就不再举例子了,如果您有兴趣的话可以到 google 上查一下相关的资料。

总结


在上面介绍的三种方案中,除方案二尚不能实现在分属于不同域的帧之间进行数据交换之外,经证明方案一和方案三都是可行的,不过这两种方案又各有利弊:

针对这两种可行方案,大家在应用时应该灵活选择,如果你比较看重数据的实时展现那么就可以考虑采用前者;而如果应用的平台无关性是你衡量应用的标准那就应该考虑采用后者。

另外,如果您有其他可选方案的话,非常感谢您能通过 Email
告知我,以填补我在这面方面的空白,谢谢!

参考资料


What does the IE "Access is Denied" error mean?
http://www.dannyg.com/ref/jsminifaq.html#q15

PROPERTY:  Document::domain
http://www.powermct.co.kr/teched/ecma/doc_domain.html
Writing Cross-Domain Web Applications
http://www.knownow.com/support/devguide/Tutorials/Cross_Domain.html
Why do I get "access denied"-error in IE when calling a function in another frame?
http://www.faqts.com/knowledge_base/view.phtml/aid/1524/fid/127

Comments: Post a Comment



<< Home

Archives

June 2004   November 2005   July 2006   August 2006  

This page is powered by Blogger. Isn't yours?