JS跨域访问解决方案总结
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
JS跨域访问解决方案总结
0引言:
跨域请求,顾名思义,就是一个站点中的资源去访问另外一个不同域名站点上的资源。
这种情况很常见,比如说通过style标签加载外部样式表文件、通过 img 标签加载外部图片、通过 script 标签加载外部脚本文件、通过 Web font 加载字体文件等等。
默认情况下,脚本访问文档属性等数据采用的是同源策略(Same origin policy)。
同源策略:如果两个页面的协议、域名和端口是完全相同的,那么它们就是同源的。
同源策略是为了防止从一个地址加载的文档或脚本访问或者设置从另外一个地址加载的文档的属性。
如果两个页面的主域名相同,则还可以通过设置document.domain 属性将它们认为是同源的。
随着 Web2.0 和 SNS 的兴起,Web 应用对跨域访问的需求也越来越多,但在脚本中进行跨域请求是受安全性限制的,Web 开发人员迫切需要提供一种更安全、方便的跨域请求方式来融合(Mashup)自己的 Web 应用。
这样做的一个好处就是可以将请求分摊到不同的服务器,减轻单个服务器压力以提高响应速度;另外一个好处是可以将不同的业务逻辑分布到不同的服务器上以降低负载。
值得庆幸的是,跨域请求的标准已经出台,主流浏览器也已经实现了这一标准。
W3C 工作组中的 Web Applications Working Group(Web 应用工作组)发布了一个Cross-Origin Resource Sharing(跨域资源共享规范)推荐规范来解决跨域请求的问题。
该规范提供了一种更安全的跨域数据交换方法。
具体规范的介绍可以访问上面提供的网站地址。
值得注意的是:该规范只能应用在类似XMLHttprequest 这样的 API 容器内。
IE8、Firefox 3.5 及其以后的版本、Chrome浏览器、Safari 4 等已经实现了 Cross-Origin Resource Sharing 规范,已经可以进行跨域请求了。
一、支持跨域访问处理浏览器
Cross-Origin Resource Sharing 的工作方式是通过添加 HTTP 头的方法来判断哪些资源允许 Web 浏览器访问该域名下的信息。
然而,对于那些 HTTP 请求导致用户数据产生副作用的请求方法(特别是对于除了GET、某些 MIME 类型的 POST 之外的 HTTP方法),该规范要求浏览器对请求进行“预先验”,通过发送 HTTP 的OPTIONS 请求头询问服务器有哪些支持的方法,在征得服务器的同意后,再使用实际的 HTTP 请求方法发送实际的请求。
服务器也可以通知客户端是否需要将验证信息(如 Cookie 和 HTTP Authentication 数据)随同请求一起发送。
下面我们就采用实际的例子说明 Cross-Origin Resource Sharing 是如何工作的。
1,简单请求
什么样的请求算是简单请求呢?简单请求必须满足下面2点:
a,只使用 GET、POST 进行的请求,这里的POST只包括发送给服务器的数据类型(Content-Type)必须是 application/x-www-form-urlencoded、
multipart/form-data 或者 text/plain中一个。
b,HTTP 请求没有设置自定义的请求头,如我们常用的 X-JSON。
先使用下面的代码进行测试:
XML/HTML代码
然后,在服务器创建 CrossDomainRequest.aspx 的内容如下:C# 代码
点击“开始测试” 按钮,发送的请求和返回的响应信息如下:
XML/HTML代码
需要特别注意的是:在请求信息中,浏览器使用 Origin 这个 HTTP 头来标识该请求来自于 http://www.meng_xian_:801;在返回的响应信息中,使用Access-Control-Allow-Origin 头来控制哪些域名的脚本可以访问该资源。
如果设置 Access-Control-Allow-Origin:*,则允许所有域名的脚本访问该资源。
如果有多个,则只需要使用逗号分隔开即可。
注意:在服务器端,Access-Control-Allow-Origin 响应头
http://www.meng_xian_:801 中的端口信息不能省略。
有人可能会想:自己发送请求头会如何呢?比如
xhr.setRequestHeader("Origin","http://www.meng_xian_:801"); 实践证明,自己设置 Origin 头是不行的。
是不是现在就可以采用 XMLHttpRequest 来请求任意一个网站的数据呢?还是不行的。
允许哪些域名可以访问,还需要服务器来设置 Access-Control-Allow-Origin 头来进行授权,具体的代码是:
Response.AddHeader("Access-Control-Allow-Origin",
"http://www.meng_xian_:801");
这行代码就告诉浏览器,只有来自 http://www.meng_xian_:801 源下的脚本才可以进行访问。
好了,上面我们就完成了一个简单的跨域请求,怎么样?感觉还是不错的吧。
下面我们进行一个“预检”请求。
2,预检请求
预检请求首先需要向另外一个域名的资源发送一个HTTP OPTIONS 请求头,其目的就是为了判断实际发送的请求是否是安全的。
下面的2种情况需要进行预检:a,不是上面的简单请求,比如使用Content-Type 为application/xml 或text/xml 的POST 请求
b,在请求中设置自定义头,比如X-JSON、X-MENGXIANHUI 等
注意:在iis 里进行测试,必须在“应用程序扩展”里面配置.aspx 扩展的动作允许OPTIONS。
下面我们举一个预检的请求:
XML/HTML代码
上面的例子我们发送xml 格式的数据,并且,发送一个非标准的HTTP头POWERED-BY-MENGXIANHUI 来说明服务器端该如何设置响应头的。
在服务器端,PreflightedRequests.aspx 的内容如下:
C#代码
点击“开始测试”按钮,将会执行下面的一系列请求。
XML/HTML代码
以上的代码反映了预检请求的执行过程:首先发送OPTIONS 请求头,用来向服务器咨询服务器的更多信息,以便为后续的真实请求做准备。
比如是否支持POST 方法等。
值得注意的是:浏览器还发送Access-Control-Request-Method: POST 和
Access-Control-Request-Headers: powered-by-mengxianhui 请求头。
注意:以上过程是第一次请求的时候的过程,如果在30 秒内重复点击按钮,你可以看不到OPTIONS 这一过程。
则执行过程是这样的:
XML/HTML代码
为什么会这样?细心的同学可能注意到了,在服务器端有一行代码Response.AddHeader("Access-Control-Max-Age", "30"); 它是用来设置预检的有效时间的,单位是秒。
这一点要特别注意。
3,带验证信息的请求
身份验证是Web开发中经常遇到的问题,在跨域请求中,默认情况下是不发送验证信息的。
要想发送验证信息,需要进行withCredentials 属性,下面就是一个简单请求的例子:
XML/HTML代码
点击“开始测试”,我们可以检测到下面的请求执行过程:XML/HTML代码
从上面的响应中可以看出,Cookie 是会随请求一起发送的。
如果我们多次点击测试按钮,则可以看到请求和响应的结果是这样的:
XML/HTML代码
注意 Cookie: _SessionId=fn2zf0zq1cuwgf45fm5fw145; visit=2 这一行,访问计数器已经被一起发送到服务器。
4,IE8中的实现方法
IE8已经开始支持跨域访问资源了,但是,IE8提供的功能还比较简单,可以进行简单的请求,下面是一个使用的例子:
XML/HTML代码
另外,IE8的实现方法与其他浏览器不同。
更多内容请参考XDomainRequest 对象,地址是:/zh-cn/library/cc288060(VS.85).aspx
最后,愿意测试的朋友可以访问这个/SimpleCrossSiteRequests.aspx 地址进行“简单请求”的测试,本页面允许任何地址进行跨域访问。
具体情况有:
一、本域和子域的相互访问: 和
二、本域和其他域的相互访问: 和用 iframe
三、本域和其他域的相互访问: 和用 XMLHttpRequest
访问代理
四、本域和其他域的相互访问: 和用 JS创建动态脚本
解决方法:
一、如果想做到数据的交互,那么和必须由你来开发才可以。
可以将用iframe添加到的某个页面下,在和iframe里面都加上document.domain = "",这样就可以统一域了,可以实现跨域访问。
就和平时同一个域中镶嵌iframe一样,直接调用里面的JS就可以了。
(这个办法我没有尝试,不过理论可行)
二、当两个域不同时,如果想相互调用,那么同样需要两个域都是由你来开发才可以。
用iframe可以实现数据的互相调用。
解决方案就是用
window.location[location是javascript里边管理地址栏的内置对象]对象的hash属性。
hash属性就是[hash是一个URL习惯用法,它是以字符 # 开始指向一个位于文档中的anchor使浏览器打开一个新的URL,一个 location对象的hash 属性是当前URL的anchor部分的名字,它由 hash 标记符和名字组成]。
利用JS改变hash值网页不会刷新,可以这样实现通过JS访问hash值来做到通信。
不过除了IE之外其他大部分浏览器只要改变hash就会记录历史,你在前进和后退时就需要处理,非常麻烦。
不过再做简单的处理时还是可以用的,具体的代码我再下面有下载。
大体的过程是页面a和页面b在不同域下,b通过iframe 添加到a里,a通过JS修改iframe的hash值,b里面做一个监听(因为JS只能修改hash,数据是否改变只能由b自己来判断),检测到b的hash值被修改了,得到修改的值,经过处理返回a需要的值,再来修改a的hash值(这个地方要注意,如果a本身是那种查询页面的话比如
http://domian/web/a.aspx?id=3,在b中直接parent.window.location是无法取得数据的,同样报没有权限的错误,需要a把这个传过来,所以也比较麻烦),同样a里面也要做监听,如果hash变化的话就取得返回的数据,再做相应的处理。
三、这种情形是最经常遇到的,也是用的最多的了。
就是和
你只能修改一个,也就是另外一个是别人的,人家告诉你你要取得数据就访问某某连接参数是什么样子的,最后返回数据是什么格式的。
而你需要做的就是在你的域下新建一个网页,让服务器去别人的网站上取得数据,再返回给你。
domain1下的a向同域下的GetData.aspx请求数据,GetData.aspx向domain2下的ResponseData.aspx发送请求,ResponseData.aspx返回数据给GetData.aspx, GetData.aspx再返回给a,这样就完成了一次数据请求。
GetData.aspx在其中充当了代理的作用。
具体可以看下我的代码。
四、这个和上个的区别就是请求是使用<script>标签来请求的,这个要求也是两个域都是由你来开发才行。
原理就是JS文件注入,在本域内的a内生成一个JS 标签,它的SRC指向请求的另外一个域的某个页面b,b返回数据即可,可以直
接返回JS的代码。
因为script的src属性是可以跨域的。
具体看代码,这个也比较简单。
code:
/files/300697/Cross_The_Site_Test_code.rar.h tml
(csdn不能粘贴附件么?)
二、对于IE7及以下版本跨域问题及方案Case I.本域和子域的相互访问:和
即用户访问A网站时所产生的对B网站的跨域访问请求均提交到A网站的指定页面,由该页面代替用户页面完成交互,从而返回合适的结果。
此方案可以解决现阶段所能够想到的多数跨域访问问题,但要求A网站提供Web代理的支持,因此A网站与B网站之间必须是紧密协作的,且每次交互过程,A网站的服务器负担增加,且无法代用户保存session状态。
Case II. on-Demand方式(本域和其他域的相互访问: 和 用 JS创建动态脚本)
MYMSN的门户就用的这种方式,不过 MYMSN中不涉及跨域访问问题。
在页面内动态生成新的<script>,将其src属性指向别的网站的网址,这个网址返回的内容必须是合法的Javascript脚本,常用的是JSON消息。
此方案存在的缺陷是,script的src属性完成该调用时采取的方式时get方式,如果请求时传递的字符串过大时,可能会无法正常运行。
不过此方案非常适合聚合类门户使用。
<html>
<head>
<script language="javascript" type="text/javascript">
function loadContent()
{
var s=document.createElement('SCRIPT');
s.src='/TestCrossJS.aspx?f=setDivContent% 27;
document.body.appendChild(s);
}
function setDivContent(v)
{
var dv = document.getElementById("dv");
dv.innerHTML = v;
}
</script>
</head>
<body>
<div id="dv"></div>
<input type="button" value="Click Me" onclick="loadContent()">
</body>
</html>
其中的/TestCrossJS.aspx是这样的,
<script language="C#" runat="server">
void Page_Load(object sender, EventArgs e)
{
string f = Request.QueryString["f"];
Response.Clear();
Response.ContentType = "application/x-javascript";
Response.Write(String.Format(@"{0}('{1}');", f, DateTime.Now));
Response.End();
}
</script>
点击“Click Me”按钮,生成一个新的script tag,下载对应的 Javascript 脚本,结束时回调其中的setDivContent(),从而更新网页上一个div的内容。
Case III. iframe方式本域和其他域的相互访问: 和 用 iframe
数据提交跟获取,采用iframe这种方式的确可以了,但由于父窗口与子窗口之间不能交互(跨域访问的情况下,这种交互被拒绝),因此无法完成对父窗口效果的影响。
在页面内嵌或动态生成指向别的网站的IFRAME,然后这2个网页间可以通过改变对方的anchor hash fragment来传输消息。
改变一个网页的anchor hash fragment并不会使浏览器重新装载网页,所以一个网页的状态得以保持,而网页本身则可以通过一个计时器(timer)来察觉自己anchor hash的变化,从而相应改变自己的状态。
1. http://domain1/TestCross.html:
<html>
<head>
<script language="javascript" type="text/javascript"> var url = "http://domain2/TestCross.html";
var oldHash = null;
var timer = null;
function getHash()
{
var hash = window.location.hash;
if ((hash.length >= 1) && (hash.charAt(0) == '#')) {
hash = hash.substring(1);
}
return hash;
}
function sendRequest()
{
var d = document;
var t = d.getElementById('request');
var f = d.getElementById('alienFrame');
f.src = url + "#" + t.value + "<br/>" + new Date(); }
function setDivHtml(v)
{
var d = document;
var dv = d.getElementById('response');
dv.innerHTML = v;
}
function idle()
{
var newHash = getHash();
if (newHash != oldHash)
{
setDivHtml(newHash);
oldHash = newHash;
}
timer = window.setTimeout(idle, 100);
}
function window.onload()
{
timer = window.setTimeout(idle, 100);
}
</script>
</head>
<body>
请求:<input type="text" id="request"> <input type="button" value="发送" onclick="sendRequest()" /><br/>
回复:<div id="response"></div>
<iframe id="alienFrame" src="http://domain2/TestCross.html"></iframe>
</body>
</html>
2. http://domain2/TestCross.html:
<html>
<head>
<script language="javascript" type="text/javascript">
var url = "http://domain1/TestCross.html";
var oldHash = null;
var timer = null;
function getHash()
{
var hash = window.location.hash;
if ((hash.length >= 1) && (hash.charAt(0) == '#'))
{
hash = hash.substring(1);
}
return hash;
}
function sendRequest()
{
var d = document;
var t = d.getElementById('request');
var f = parent;
//alert(f.document); //试着去掉这个注释,你会得到“Access is denied”f.location.href = url + "#" + t.value + "<br/>" + new Date();
}
function setDivHtml(v)
{
var d = document;
var dv = d.getElementById('response');
dv.innerHTML = v;
}
function idle()
{
var newHash = getHash();
if (newHash != oldHash)
{
setDivHtml(newHash);
oldHash = newHash;
}
timer = window.setTimeout(idle, 100);
}
function window.onload()
{
timer = window.setTimeout(idle, 100);
}
</script>
</head>
<body>
请求:<input type="text" id="request"> <input type="button" value="发送" onclick="sendRequest()" /><br/>
回复:<div id="response"></div>
</body>
</html>
两个网页基本相同,第一个网页内嵌一个IFRAME,在点击“发送”按钮后,会将文本框里的内容通过hash fragment传给IFRAME。
点击IFRAME里的“发送”按钮后,它会将文本框里的内容通过hash fragment传给父窗口。
因为是只改动了hash fragment,浏览器不会重新load网页内容,这里使用了一个计时器来检测URL变化,如果变化了,就更新其中一个div的内容。
Case IV.本地转储方式 widows剪切板访问[比较土少量数据应用]
IE本身依附于windows平台的特性为我们提供了一种基于iframe,利用内存来“绕行”的方案,即两个window之间可以在客户端通过windows剪贴板的方式进行数据传输,只需要在接受数据的一方设置Interval进行轮询,获得结果后清除Interval即可。
FF的平台独立性决定了它不支持剪贴板这种方式,而以往版本的FF中存在的插件漏洞又被fixed了,所以FF无法通过内存来完成暗渡陈仓。
而由于文件操作FF 也没有提供支持(无法通过Cookie跨域完成数据传递),致使这种技巧性的方式只能在IE中使用。
window.clipboardData.setData("text","克隆");
window.clipboardData.getData("text");//获取剪切版内容
剪切板是指windows 操作系统提供的一个暂存数据,并且提供共享的一个模块。
也称为数据中转站,剪切板在后台起作用,在内存里.,是操作系统设置的一段存储区域,你是在硬盘里找不到的.你只要有文本输入的地方按CRTL+V或右键粘贴就出现了,.新的内容送到剪切板后,将覆盖旧内容。
即剪切板只能保存当前的一份内容,因在内存里,所以,电脑关闭重启,存在剪切板中的内容将丢失。
Case V: (其实还是在服务端A用iframe解决了与服务器B通信)
要解决的问题:发生在用户提交网页URL(还包括Tag, Notes等)给Bookmark服务器时。
关于URL的提交至少可以有三种方式:
1.登陆Bookmark服务器的提交页面,将要收藏的URL通过该页面提交给服务
器。
2.安装浏览器插件,通过插件将URL提交给服务器。
3.从Bookmark服务器动态加载javascript小工具到当前页面,通过它来完
成提交工作。
第一种方式开发起来最简单,但对用户来讲比较麻烦,每次都需要先登陆Bookmark服务器才能完成提交;第二种方式我并不熟悉插件开发,而且用户也不喜欢太多的插件堆满自己的浏览器;第三种方式开发难度小,又避免了每次登陆服务器的麻烦,所以最终采用它。
第三种方式中动态加载的javascript小工具除了需要生成UI供用户填写信息(URL,tag,notes等),当用户点击提交的时候,还要完成与服务器通信的功能。
跨域访问,简单来说就是A网站的javascript代码试图访问B网站,包括提交内容和获取内容。
由于安全原因,跨域访问是被各大浏览器所默认禁止
的。
写过跨域访问ajax的朋友相信都遇到过被告知“没有权限”的情况。
通过XMLHttp来发送数据给Bookmark服务器的尝试失败了。
于是,看到网上的一些资料,我又开始尝试用javascript小工具在用户网页动态创建一个隐藏的iframe, iframe的src指向服务器的一个servlet,试图通过调用iframe 中提供的javascript来完成与服务器的通信。
但不幸的是,用户网页中的javascript代码访问iframe也被浏览器归为跨域访问(特指iframe的src指向其它网站的情形),尝试再次失败。
最终,在一篇文章中看到,与iframe不同,如果A网站从B网站加载javascript,A网站可以自由的访问该javascript的内容,并不会被浏览器认为是跨域访问。
模仿刚才iframe的思路,当用户点击提交时,可以动态创建一个javascript对象,该对象的src指向Bookmark服务器的一个servlet,注意:URL、Tag、Notes、User、Password等信息被作为src URL参数传给服务器。
请看下面的代码:
var url = "http://localhost:8080/Deeryard/BookmarkServlet?" +
"url=" + url_source + "&" + "title=" + title + "&" +
"tag=" + tag + "&" + "notes=" + notes + "&" + "user=" + user+ "&" +
"password=" + password;
url = encodeURI(url);
//Submit to server with a trick
var js_obj = document.createElement("script");
js_obj.type = "text/javascript";
js_obj.setAttribute("src", url);
//Get response from server by appending it to document
document.body.appendChild(js_obj);
上面例子中,js_obj.setArrribute()将信息作为src的URL参数提交给了Bookmark servlet。
那么用户又如何取得服务器的响应信息呢?答案就是最末一行代码,servlet的输出必须是javascript代码,它可以调用用户网页上的其他javascript函数,以及操作dom对象。
下面的servlet代码生成了一个javascript函数调用:
out.write("onServerResponse(INADEQUATE_INFORMATION);");
document.body.appendChild(js_obj)执行后
onServerResponse(INADEQUATE_INFORMATION)就会得到执行,使客户网页响应服务器结果。
这样一个完整的通信过程就完成了。
解决方法小结:
方法一:设定domain属性
只适合情形1。
Document对象的domain属性一定程度上缓解了多个二级域名时带来的不便,只要local和remote的domain属性都设为同一个域()就可以互相访问了。
不过只是一定程度上的,只能接受两个域共有的部分(如不能设成反过来也不可,只能为),还要保持至少有一个“点号”。
方法二:window共享内存
Windows剪切板访问
方法三:js脚本植入
1和2都适用,在local端的页面中插入一个在remote端部署的动态脚本生成程序,然后通过将请求命令编码成url作为包含脚本的src,响应端的程序处理url查询命令返回特定的javascript代码(包含操作或者数据),代码在插入时被浏览器解析并执行。
这也是baidu在用的方法。
使用<script>标签来请求的,这个要求也是两个域都是由你来开发才行。
原理就是JS文件注入,在本域内的a 内生成一个JS标签,它的SRC指向请求的另外一个域的某个页面b,b返回数据即可,可以直接返回JS的代码。
因为script的src属性是可以跨域的。
方法四:没有办法的办法也是最正当的方法proxy
适合全部三种情形也是解决情形3的唯一办法。
就是用可以跨域的服务器语言编写一个代理proxy,放在local端。
请求时ajax请求本地的proxy来读取远端的数据再返回给请求端。
不破坏同源策略而且实现起来容易,我比较爱用。
唯一局限是要受服务器性能限制。
方法五:通过iframe与hash来传递[实践证明是错误的方案]
1和2都适用。
可以将要请求的remote页面包含在local页面的iframe中,由于iframe 与父框架可以互相设定hash值这样就可以在2个页面中都设定监听方法setInterval 来捕获hash值的变化,不过remote端捕获的是请求命令而local端捕获的是请求结果。
责任在我没有进行充分的测试,回来后重新彻底的模拟了相关的环境,证实了这个错误。
非常非常感谢腾讯的这位朋友。
这个方法的前提也只能是在情形一下,但必须也要事先设定domain属性。
参考资料:
0.Cross-Origin Resource Sharing规范
/TR/access-control/和/2006/waf/access-control/
1. Security Considerations: Dynamic HTML
/library/default.asp?url=/workshop/author/dh tml/sec_dhtml.asp
2. About Cross-Frame Scripting and Securityurl
/library/default.asp?url=/workshop/author/om /xframe_scripting_security.asp
3. Cross-Domain Proxy
/Cross-Domain_Proxy
4. Cross Domain XMLHttpRequest using an IFrame Proxy
/WikiHome/DojoDotBook/Book75
5. Back Button Support for Atlas UpdatePanels
/BackButtonSupport.aspx
6. Cross-document messaging hack
/archives/000304.html
7. Building Mash-ups with "Atlas"
/docs/Walkthroughs/DevScenarios/bridge.aspx
8. Calling web services hosted outside of your application with “Atlas”/federaldev/archive/2006/07/31/684229.aspx
/Shared%20Documents/Presentations%20by %20Marc%20Schweigert/CallAtlasWebServiceInDifferentProject.zip
9. AJAX Tip: Passing Messages Between iframes
/weblog/PermaLink.aspx?guid=3b03cf9d-b589-4 838-806e-64efcc0a1a15
10. OSCON Cross-site Ajax Slides
/archives/2006/07/oscon_crosssite.html
/css/api/Joseph-Smarr-Plaxo-OSCON-2006.ppt
11. OSCON 2006: Cross-site Ajax
/blogs/2006/07/28/oscon-2006-cross-site-ajax/。