跨域引用资源技术及其方案选型

来源:互联网 发布:地图数据 编辑:程序博客网 时间:2024/05/16 02:13

感觉文章写的不错,虽然好些还是看不懂,先收藏


原文地址:http://truemylife.iteye.com/blog/1454300

一、源

         同源策略是浏览器都必须遵循的策略,这就限制了js去调用和修改非同域下的数据。试想如果没有这个策略,在另外一个域的js就能轻易修改当前你正在调用的页面。那就天下大乱,毫无安全可言了。遵循同源策略就对非同域的资源调用加上了许多限制。 我们知道css、image、及js本身(指文件本身)是可以跨域引用的。但html文件和js具体的数据调用如ajax是不能跨域了。还有,所谓同源是指两个页面的协议、域名、端口完全相同,否则都算跨域。通常我们会有很多二级域名,这时需要设置document.domain与主域名一致,否则也认为是跨域。

   

二、典型的资源共享案例

1)、html做为模板,想被各系统共用。(主次域名)

2)、公开的数据接口想被各系统调用。(不同域名)

3)、请求第三方url结果。(不同域名)

这里先列出这三种需求,因为不同的需求,技术选型也不一样。

 

三、Cross-Origin Resource Sharing

         这是W3C推出的跨域资源标准规范,其核心部分是通过HTTP请求头来控制跨域的安全性,主要有两个头描述,一是Origin:描述当前的资源是可用来跨域请求的;另外一个是Access-Control-Allow-Orgin:用来描述哪些域是可以访问该资源,允许设置多个。

由于IE8之前的版本及其他浏览器稍低些的版本不遵循此规范,从当前来看其应用还不广泛,因此暂不考虑用此方案来解决上述三个案例。如要对Cross-Origin Resource Sharing进官方进一步了解:http://www.w3.org/TR/cors/。

 

四、跨域请求资源方案

1、配置反向代理

            比如你的资源在ip/resource/resource.html下,中间件只配置resource目录给resource.caidao8.com。现在              有www.caidao8.com、practise.caidao8.com都想跨域调用resource.html。可以通过配置中间件,把ip/resource目录同时配置到域resource.caidao8.com、www.caidao8.com下。这样在resource.caidao8.com要调用resource.html时,就直接这样引用http://resource.caidao8.com/resource/resource.html,同样的在www.caidao8.com要调用时这样引用http://www.caidao8.com/resource/resource.html。实际上如果你有其他不同的域,比如www.caidao8.cn,也可以把资源配置一份到www.caidao8.cn域下。就看你拥有几个域并配置它们。

         结论:通过中间件配置让共用资源变成多种域名下可访问是解决跨域的一种有效方法。但方案受限于共享的资源完全由自己控制。还有像tomcat、jetty本身一般不做反向代理,如果要在开发环境查看效果和调试,就得给开发环境也安装nginx/apache,这增加了开发环境的复杂性,考虑到整个团队实施问题,在实际中我们团队没有采用这种方案。

2、IFrame解决跨域问题及其不足之处

         IFrame解决跨域问题,只限于a、b是主次域名,首先设置document.domain使主次域名统一。我们设定a是parent,b是放在a下的iframe源。首先在b页面加上一段

<script>$(document).attr(“domain”,”caidao8.com”);</script>(本文所有js代码以jquery表示)。在a下编写js代码:

 

 

Java代码  收藏代码
  1. $jq(document).attr("domain","caidao8.com");  
  2. //jquery找不到object,会自动创建此对象  
  3. $jq("<iframe id='dddiframe'></iframe>").attr("src","http://testresource.caidao8.com/billtemplate/aaa.html").appendTo("body");  
  4. //后续事件必须等iframe资源加载完成后才能操作,而不同的浏览器对iframe资源是否加载完成事件不一致  
  5. //iframe下使用jquery的bind与javascript原意attachEvent,两者执行顺序不同,使用jquery bind绑定未果,采用js Dom对象来操作  
  6. var iframe = $jq("#dddiframe")[0];  
  7. if (iframe.attachEvent){ //解决ie不认iframe onload问题   
  8.      iframe.attachEvent("onload", function(){  
  9.      alert($jq("#dddiframe").contents().find("#billid").html());  
  10.      });   
  11.  } else {   
  12.      iframe.onload = function(){  
  13.      //FF及Chrome能正常获取  
  14.      alert($jq("#dddiframe").contents().find("body").html());  
  15.      };   
  16.  }  

         注意弯路:因为所请求b的资源是静态资源,一般情况下服务端和浏览器客户端会有缓存,特别是Chrome浏览器,我在开发的过程中,为请求的b资源加了一段

<script>$(document).attr(“domain”,”caidao8.com”);</script>,FF和IE在强刷请求入口后(没有直接强刷b的url,而是强刷a的入口),都产生作用,在Chrome下,强刷a入口,iframe里请求的b资源却还是一直是本地的缓存。这个问题困扰了整整一天,搜了很多资料,还以为是Chrome的一个bug(在Chrome官方也有人提出问题,但没有确认是bug),因为一开始我使用的是12.0.742.91 m版本,使用调试器并没有提示任何出错信息,而是直接在调试器里看到iframe下的contentDocument对象为 undefined;升级Chrome到版本17.0.963.79 m后还是有进步,再进入调试器,这回调试器给出了报错信息,提示协议、域及端口要匹配,这才注意到,原来b资源一直还是本地缓存的资源,直接强刷b资源后,终于把Chrome也搞定了。

         结论:如果用iframe来实现跨域访问,数据获取被推到了前端JS,而绕过了服务器,节省服务端资源开销。而且数据直接在客户端交互,速度也最快。在我开发的过程中,请求a的入口http://www.caidao8.com:8080/test而上面的b资源地址是http://testresource.caidao8.com/billtemplate/aaa.html,这两者的端口并不一致,但浏览器似乎有放低同源策略port这个约束标准,这样的端口差异是能被放行的。

Iframe跨域的应用场景受限于主次域名,网上有一种说法,就是a、b两个非主次域名的也通过iframe来实现跨域访问,实际上并不好实现。另外要对所有要引用资源跨域HTML资源加上document.domain=”yourdomain.com”,这无疑也是件麻烦的事情。

 

3、JSONP

JSONP本质上是一种Javascript注入,通过ajax的方式请求并回调注入服务端数据,它要求服务端提供标准的json格式的数据。

这是freemarker定义的一个标准jsonp返回数据模板样例:

 

Java代码  收藏代码
  1. ${callback!}({"options":[  
  2. <#if datadicts?? && datadicts?size  &gt; 0>  
  3.    <#list datadicts as datadict>  
  4.     {  
  5.        "value":"${datadict.key}","text":"${datadict.value}"  
  6.     }  
  7.      <#if datadict_has_next>,</#if>  
  8.     </#list>  
  9. </#if>  
  10. ]});  

  callback参数就是ajax端请求的回调函数,datadicts是服务端定义的数据,这里略去了JAVA部分的代码,你可以写个Servlet或是Action,把callback及要传出数据按json格式输出。这里用freemarker取例是因为其模板内容看起来更加直观,你也可以直接用servlet response输出你的内容。

 

再贴PHP写的JSONP服务端代码样例:

 

Java代码  收藏代码
  1. <?php  
  2. // create a new curl resource  
  3. function geturlcontent(){  
  4. $ch = curl_init();  
  5.    
  6. // set URL and other appropriate options  
  7. curl_setopt($ch, CURLOPT_URL, "http://testresource.caidao8.com/billtemplate/ec51de3cd33749aa8401c9b6854ec2d6.html");  
  8. curl_setopt($ch, CURLOPT_HEADER, 0);  
  9. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);//不直接输出,返回到变量  
  10. $content = curl_exec($ch);  
  11. curl_close($ch);  
  12. //mb_convert_encoding($content,'utf-8');  
  13. return json_encode($content);  
  14. }  
  15. $fun = $_GET["callback"];  
  16. $output = $fun."({\"htmlstr\":".geturlcontent()."})";  
  17. echo $output;  
  18. ?>  
  19.    

 Jquery调用jsonp样例:

 

 

Java代码  收藏代码
  1. $jq.getJSON("http://localhost/dong/jsonp/getbilltemplate.php?callback=?",function(data){  
  2.        $jq("#saveid").before(data.htmlstr);  
  3.        //因为getJSON默认是异步的,因此必须把后续业务代码跑在这里。  
  4. });  
  5.    

  结论:为了做比较,本人在开发的过程中特意把iframe实现的跨域请求html功能,用jsonp来实现,之所以使用PHP,是因为PHP在Nginx/apache下能直接解析,因为HTML资源本身与业务无关,这样可以省去java中间件及jvm虚拟机层。由于HTML本身内容不符合json格式,因此在输出时,需要json_encode()方法,经测试发现这样的效率远低于iframe跨域会规,也比不上下面要讲的请求代理方式。因此在请求HTML资源时使用jsonp方式是不合适的。那如果数据本身不是html资源,而是普通的数据,jsonp用来跨域传输公开数据倒是一种既简单又实用方案。jsonp可以在任何不同的域之间调用,前提是你要相信服务端的数据是可信赖和非恶意的,对于服务端来说其数据接口必须是公开的。

 

 

4、Open API

    目前大型的网站基本上开放或部分开放其API,甚至有人直接把开放API说成云概念,从服务端的角度来理解,开放的API可以理解成是JSONP的进一步扩展,加入了认证校验安全方面的业务。当然服务端可能会进一步收集客户端的流量等数据,对客户端的资源访问做进一步控制。通常API的数据格式会支持JSON、Rest、标准XML格式,相对于JSONP的简单处理,开放API在服务端的设计上要复杂一些,客户端调用一般也需要申请并通过认证后才能调用接口,当然可能有一部分接口是完全公开的。从客户端的角度来说,开放api与jsonp的处理就完全不一样了,开放api需要客户端自己写解析器去解析,因为json、rest风格基本上是一致的,因此也可以到网上直接获取高效的解析器。这里不进一步讲述api服务端及客户端详细实现。

    结论:如果服务端想开放数据及数据接口,又有较复杂的权限及资源等业务方面的控制,那无疑要选择Open API。但个人认为在公司内部不同的项目之间,采用开放API的方式也更加有利于代码的集中管理及应用优化。

 

5、请求代理

    这里的请求代理很简单,实际上就是模拟Http请求,然后获取到请求结果后,再转成服务输出。一段JAVA代码样例:

 

Java代码  收藏代码
  1. import org.apache.commons.httpclient.HttpClient;  
  2. import org.apache.commons.httpclient.HttpException;  
  3. import org.apache.commons.httpclient.HttpStatus;  
  4. import org.apache.commons.httpclient.methods.GetMethod;  
  5. //这里省去一些代码  
  6. public void getbilltemplate(){  
  7. HttpClient httpClient = new HttpClient();  
  8.  //创建GET方法的实例  
  9.  GetMethod getMethod = new GetMethod(Configuration.get("webapp.resource.mediaRoot","http://testresource.caidao8.com")+"/billtemplate/ec51de3cd33749aa8401c9b6854ec2d6.html");  
  10. //使用系统提供的默认的恢复策略  
  11.   getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,  
  12. //         new DefaultHttpMethodRetryHandler());  
  13.          try {  
  14. //           getRequest().setCharacterEncoding("UTF-8");  
  15.           //执行getMethod  
  16.           int statusCode = httpClient.executeMethod(getMethod);  
  17.           if (statusCode != HttpStatus.SC_OK) {  
  18.            System.err.println("Method failed: "  
  19.              + getMethod.getStatusLine());  
  20.           }  
  21.            //读取内容  
  22.           byte[] responseBody = getMethod.getResponseBody();  
  23.            
  24.           writeAjax(new String(responseBody,"UTF-8"));  
  25.            
  26.          } catch (HttpException e) {  
  27.           //发生致命的异常,可能是协议不对或者返回的内容有问题  
  28.           System.out.println("Please check your provided http address!");  
  29.           e.printStackTrace();  
  30.          } catch (IOException e) {  
  31.           //发生网络异常  
  32.           e.printStackTrace();  
  33.          } finally {  
  34.           //释放连接  
  35.           getMethod.releaseConnection();  
  36.          }  
  37.     }  
  38.    

  这段代码的作用,就是先请求url:http://testresource.caidao8.com/billtemplate/ec51de3cd33749aa8401c9b6854ec2d6.html,然后再把获取到的内容输出。你可以把这里的url换成任何你想要的url。这样通过这个代理,你就能获取到你想要的其他站点的资源内容。当然这样获取的资源内容大都就是一堆html内容输出,如果你要获取里面的数据,需要进一步解析内容。下面是用ajax请求这个代理输出

 

Java代码  收藏代码
  1. $jq.ajax({  
  2.        url:"/billstudy/getbilltemplate.hx",  
  3.        datatype:"html",  
  4.        cache:false,  
  5.        async:false,  
  6.         success:function(data){  
  7.         $jq("#saveid").before(data);  
  8.         }  
  9.  });  

     由于直接使用datatype:”html”,其效率还是可以接受的。

   结论:请求代理可以获取站外任何公开资源,如果进一步抽象和优化,这不就是爬虫的原型么!

 

五、选型

回到第二大点的三种典型案例,我们可以给出选型答案:

典型的资源共享案例

选型参考

html做为模板,想被各系统共用。(主次域名)

Iframe跨域方案

如果不考虑开发阶段的复杂性:选择配置反向代理方案更加直接。

公开的数据接口想被各系统调用。(不同域名)

如果是一般数据:jsonp

复杂业务控制的数据:open API

如果是html数据:请求代理

请求第三方url结果。(不同域名)

请求代理

 

把五种解决方案作列表对比:

解决方案

场景描述

配置反向代理

共享资源属于自己内部,可以把资源随意配置到你拥有域名下。限于文件资源。

Iframe跨域

限于主次域名的文件资源。

jsonp

完全跨域并完全公开的json格式数据传输。

Open API

包含较复杂业务控制及约定的数据传输。

请求代理

完全跨域的url内容抓取代理。