解决ajax跨域的方法原理详解

1、神马是跨域(Cross Domain)


对于端口和协议的不同,只能通过后台来解决。

一句话:同一个ip、同一个网络协议、同一个端口,三者都满足就是同一个域,否则就是

跨域问题了。而为什么开发者最初不直接定为一切可跨域的呢?默认的为什么都是不可跨域呢?这就涉及到了同源策

略,为了系统的安全,由Netscape提出一个著名的安全策略。现在所有支持JavaScript的浏览器都会使用这个策略。

所谓同源是,域名,协议,端口相同。当我们在浏览器中打开百度和谷歌两个网站时,百度浏览器在执行一个脚本的

时候会检查这个脚本属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行,如果没有同源策略,那

随便的向百度中注入一个js脚本,弹个恶意广告,通过js窃取信息,这就很不安全了。




说白点就是post、get的url不是你当前的网站,域名不同。例如在aaa.com/a.html里面,表单的提交action是bbb.com/b.html。

不仅如此,www.aaa.com和aaa.com之间也属于跨域,因为www.aaa.com是二级域名,aaa.com是根域名。

JavaScript出于安全方面的考虑,是不允许跨域调用其他页面的对象的(同源策略 Same-Origin Policy)。

关于JavaScript能否跨域通信的详细说明,见下表:

http://www.a.com/a.js访问以下URL的结果

URL 说明 是否允许通信
http://www.a.com/b.js 同一域名下 允许
http://www.a.com/script/b.js 同一域名下不同文件夹 允许
http://www.a.com:8000/b.js 同一域名,不同端口 不允许
https://www.a.com/b.js 同一域名,不同协议 不允许
http://70.32.92.74/b.js 域名和域名对应ip 不允许
http://script.a.com/b.js 主域相同,子域不同 不允许
http://a.com/b.js 同一域名,不同二级域名(同上) 不允许
http://www.b.com/b.js 不同域名 不允许

 

2、为嘛要跨域

跨域这东西其实很常见,例如我们可以把网站的一些脚本、图片或其他资源放到另外一个站点。例如我们可以使用Google提供的jQuery,加载时间少了,而且减少了服务器的流量,如下:

1
<script type="text/java script" src="https://aja x.googleapis.com/aj ax/libs/jquery/1.4.2/jquery.min.js">script>

有时候不仅仅是一些脚本、图片这样的资源,我们也会希望从另外的站点调用一些数据(有时候是不得不这样),例如我希望获取一些blog的RSS来生成一些内容,再或者说我在“人人开放平台”上开发一个应用,需要调用人人的数据。

然而,很不幸的是,直接用XMLHttpRequest来Get或者Post是不行的,例如我用jQuery的$.get去访问本小博的主域名 :

1
2
3
4
$.get( "http://flycoder.org/" ,
{},  function (data){
alert( '跨域不是越狱:' +data)
},  "html" );

结果如下(总之就是不行啦~FF不报错,但是木有返回数据)

那咋么办捏?(弱弱的说,测试的时候我发现IE访问本地文件时,是可以跨域的,不过这也没啥用~囧~)

3、肿么跨域

下面为了更好的讲解和测试,我们可以通过修改hosts文件来模拟跨域的效果,hosts文件在C:\Windows\System32\drivers\etc 文件夹下。在下面加3行:

127.0.0.1 www.a.com

127.0.0.1 a.com

127.0.0.1 www.b.com

3.1、跨域代理

一种简单的办法,就是把跨域的工作交给服务器,从后台获取其他站点的数据再返回给前台,也就是跨域代理(Cross Domain Proxy)。

这种方法似乎蛮简单的,改动也不太大。不过就是http请求多了些,响应慢了些,服务器的负载重了些~

 

3.2、document.domain+iframe

对于主域相同而子域不同的例子,可以通过设置document.domain的办法来解决。

举www.a.com/a.html和a.com/b.html为例,只需在a.html中添加一个b.html的iframe,并且设置两个页面的document.domain都为’a.com’(只能为主域名),两个页面之间即可互相访问了,代码如下:

www.a.com/a.html中的script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
document.domain= 'a.com' ;
var  ifr = document.createElement( 'iframe' );
ifr.src =  'http://a.com/b.html' ;
ifr.style.display =  'none' ;
document.body.appendChild(ifr);
ifr.onload =  function (){
   //获取iframe的document对象
   //W3C的标准方法是iframe.contentDocument,
   //IE6、7可以使用document.frames[ID].document
   //为了更好兼容,可先获取iframe的window对象iframe.contentWindow
   var  doc = ifr.contentDocument || ifr.contentWindow.document;
   // 在这里操纵b.html
   alert(doc.getElementById( "test" ).innerHTML);
};

 

a.com/b.html

1
2
3
4
5
6
7
8
9
10
11
12
< html >
< head >
< title > title >
< script  type = "text/javascript" >
   document.domain='a.com';
script >
head >
< body >
< h1  id = "test" >Hello World h1 >
body >
html >

如果b.html要访问a.html,可在子窗口(iframe)中通过window.parent来访问父窗口的window对象,然后就可以为所欲为了(window对象都有了,还有啥不行的),同理子窗口也可以和子窗口之间通信。

于是,我们可以通过b.html的XMLHttpRequest来获取数据,再传给a.html,从而解决跨子域获取数据的问题。

但是这种方法只支持同一根域名下的页面,如果不同根域名(例如baidu.com想访问google.com)那就无能为力了。


3.3、动态script标签(Dynamic Script Tag)

2. 因为script的src属性是可以跨域的 可以调用跨域的js文件

这里我们需要用到JQuery.getScript(url, callback)方法,url是脚本文件的URL路劲,callback函数在脚本资源已被加载和求值后调用的回调函数。

首先在bb.com创建一个js文件,test.js

1 var ojb = {msg:'js跨域请求成功'};

2  然后在aa.com的页面上使用$.getScript加载test.js脚本

1 $(function() {
2             $.getScript('http://www.bb.com/test.js', function() {
3                 if (ojb) {
4                     alert(obj.msg);                
5                 }
6             });
7         });

使用$.getScript函数的最大好处就是可以保证,脚本加载完毕后调用回调函数。

这种方法也叫“动态脚本注入”。详情   http://www.xml.com/pub/a/2005/12/21/json-dynamic-script-tag.html

这种技术克服了XMLHttpRequest的最大限制,也就是跨域请求数据。直接用JavaScript创建一个新的脚本标签,然后设置它的src属性为不同域的URL。

www.a.com/a.html中的script

1
2
3
4
5
var  dynScript = document.createElement( 'script' );
dynScript.src =  'http://www.b.com/b.js' ;
dynScript.setAttribute( "type" "text/javascript" );
document.getElementsByTagName( 'head' )[0]
     .appendChild(dynScript);

通过动态标签注入的必须是可执行的JavaScript代码,因此无论是你的数据格式是啥(xml、json等),都必须封装在一个回调函数中。一个回调函数如下:

www.a.com/a.html中的script

1
2
3
4
function  dynCallback(data){
   //处理数据, 此处简单示意一下
   alert(data.content);
}

在这个例子中,www.b.com/b.js需要将数据封装在上面这个dynCallback函数中,如下:

1 dynCallback({content:'Hello World'})

我们看到了让人开心的结果,Hello World~

不过动态脚本注入还是存在不少问题的,下面我们拿它和XMLHttpRequest来对比一下:

  XmlHttpRequest Dynamic Script Tag
跨浏览器兼容 No Yes
跨域限制 Yes No
接收HTTP状态 Yes No (除了200)
支持Get、Post Yes No (GET only)
发送、接收HTTP头 Yes No
接收XML Yes Yes
接收JSON Yes Yes
支持同步、异步 Yes No (只能异步)

 

可以看出,动态脚本注入还是有不少限制,只能使用Get,不能像XHR一样判断Http状态等。

而且使用动态脚本注入的时候必须注意安全问题。因为JavaScript没有任何权限与访问控制的概念,通过动态脚本注入的代码可以完全控制整个页面,所以引入外部来源的代码必须多加小心。

3.4、iframe+location.hash

这种方法比上面两种稍微繁琐一点,原理如下:

www.a.com下的a.html想和www.b.com下的b.html通信(在a.html中动态创建一个b.html的iframe来发送请求);

但是由于“同源策略”的限制他们无法进行交流(b.html无法返回数据),于是就找个中间人:www.a.com下的c.html(注意是www.a.com下的);

b.html将数据传给c.html(b.html中创建c.html的iframe),由于c.html和a.html同源,于是可通过c.html将返回的数据传回给a.html,从而达到跨域的效果。

三个页面之间传递参数用的是location.hash(也就是www.a.html#sayHello后面的’#sayHello’),改变hash并不会导致页面刷新(这点很重要)。

具体代码如下:

www.a.com/a.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//通过动态创建iframe的hash发送请求
function  sendRequest(){
   var  ifr = document.createElement( 'iframe' );
   ifr.style.display =  'none' ;
   //跨域发送请求给b.html, 参数是sayHello
   ifr.src =  'http://www.b.com/b.html#sayHello' ;
   document.body.appendChild(ifr);
}
//获取返回值的方法
function  checkHash() {
   var  data = location.hash ?
      location.hash.substring(1) :  '' ;
   if  (data) {
     //处理返回值
     alert(data);
     location.hash= '' ;
   }
}
//定时检查自己的hash值
setInterval(checkHash, 2000);
window.onload = sendRequest;

 

www.b.com/b.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function  checkHash(){
   var  data =  '' ;
   //模拟一个简单的参数处理操作
   switch (location.hash){
     case  '#sayHello' : data =  'HelloWorld' ; break ;
     case  '#sayHi' : data =  'HiWorld' ; break ;
     default break ;
   }
   data && callBack( '#' +data);
}
function  callBack(hash){
   // ie、chrome的安全机制无法修改parent.location.hash,
   // 所以要利用一个中间的www.a.com域下的代理iframe
   var  proxy = document.createElement( 'iframe' );
   proxy.style.display =  'none' ;
   // 注意该文件在"www.a.com"域下
   proxy.src =  'http://www.a.com/c.html' +hash;
   document.body.appendChild(proxy);
}
window.onload = checkHash;

 

www.a.com/c.html

1
2
3
4
5
//因为c.html和a.html属于同一个域,
//所以可以改变其location.hash的值
//可通过parent.parent获取a.html的window对象
parent.parent.location.hash =
     self.location.hash.substring(1);

可能有人会有疑问,既然c.html已经获取了a.html的window对象了,为何不直接修改它的dom或者传递参数给某个变量呢?

原因是在c.html中修改 a.html的dom或者变量会导致页面的刷新,a.html会重新访问一次b.html,b.html又会访问c.html,造成死循环……囧呀~

所以只能通过location.hash了。这样做也有些不好的地方,诸如数据容量是有限的(受url长度的限制),而且数据暴露在url中(用户可以随意修改)……

 

3.5、postMessage(html5)

html5中有个很酷的功能,就是跨文档消息传输(Cross Document Messaging)。新一代浏览器都将支持这个功能:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 。

使用方法如下:

1 otherWindow.postMessage(message, targetOrigin);

说明:

  • otherWindow: 对接收信息页面的window的引用。可以是页面中iframe的contentWindow属性,window.open的返回值等。
  • message: 所要发送的数据,string类型。
  • targetOrigin: 用于限制otherWindow,“*”表示不作限制。

www.a.com/a.html中的代码:

html:

1 <iframe id="ifr" src="http://www.b.com/b.html">iframe>

script:

1
2
3
4
5
6
window.onload =  function () {
     var  ifr = document.getElementById( 'ifr' );
     // 若写成'http://www.c.com'就不会执行postMessage了
     var  targetOrigin =  'http://www.b.com' ;
     ifr.contentWindow.postMessage( 'sayHello' , targetOrigin);
};

www.b.com/b.html的script

1
2
3
4
5
6
7
8
//通过message事件来通信,实在太爽了
window.addEventListener( 'message' function (e){
   // 通过origin属性判断消息来源地址
   if  (e.origin ==  'http://www.a.com'  &&
     e.data== 'sayHello' ) {
     alert( 'Hello World' );
   }
},  false );

 

3.6、使用flash

由于本人对flash不怎么熟悉,此处暂时忽略之~

 

3.7、Cross Frame

行文至此,突然在口碑网UED博客上看到了一篇 《跨域资源共享的10种方式》,对跨域的多种方法都有介绍(虽然有源码,但多数都是直接调用YUI库的,比较难看出原理)。

里面提到了Cross Frame这种方法,似乎挺不错的,改日一定翻源码来研究。


跨域  :


1.jsonp 

简易说明,JSONP就是包装的JSON,把P理解为package更为合适。例JSON和JSONP:

 
  
  1. // JSON
  2. {
  3. "s": "b"
  4. };
  5.  
  6. // JSONP
  7. jsonp({
  8. "s": "b"
  9. });

如例,JSONP中JSON是jsonp方法的实参,这样写的目的是,打开这个JSONP就运行了jsonp方法。如我们单独请求该脚本地址:

 
  

出现预想的错误,jsonp方法未定义,证明了以上说的是正确的。所以在处理JSONP的时候,我们需要预先定义一个全局函数jsonp,然后在该函数中返回实参即可。即:



$(document).ready(function(){
   var url='http://localhost:8080/WorkGroupManagment/open/getGroupById"
    
   +"?id=1&callback=?';
   


$.ajax({
     url:url,
     dataType:'jsonp',
     processData: false, 
     type:'get',
     success:function(data){
       alert(data.name);
     },
     error:function(XMLHttpRequest, textStatus, errorThrown) {
       alert(XMLHttpRequest.status);
       alert(XMLHttpRequest.readyState);
       alert(textStatus);
     }});
   });







    String callback = request.getParameter("callback");
   
    String json = JsonConverter.bean2Json(result);
    response.setContentType("text/html");
    response.setCharacterEncoding("utf-8");
    PrintWriter out = response.getWriter();
    out.print(callback + "(" + json + ")");
    return null;
  }


getJson和jsonp是一样的


$.getJSON("http://api.taobao.com/apitools/ajax_props.do&jsoncallback=?",
    function (data) {
        alert(data);
    }
);




3.2、VAR

JSONP是利用全局方法来实现跨域读取,当然也可以利用全局变量来实现跨域读取。例:

 
  
    1. var window.testvar = "123";
    2. alert(window.testvar);

并且这个方法比JSONP要来的更加简单一点,具体实现方法例

  1. function getTime(callback) {
  2. var url = "http://qianduanblog.duapp.com/test/index.php?var=testvar";
  3. var script = document.createElement("script");
  4. var head = document.getElementsByTagName("head")[0];
  5. script.src = url;
  6. script.onload = function() {
  7. // 回调
  8. callback(testvar);
  9. // 移除该script
  10. script.parentNode.removeChild(script);
  11. // 删除该script
  12. script = null;
  13. // 删除变量
  14. window.testvar = null;
  15. }
  16. // 页面上插入该脚本
  17. head.appendChild(script);
  18. }
  19. getTime(function(json) {
  20. alert(json.time);
  21. });

3.3、修缮与扩展

在跨域读取动态内容,无论是利用JSONP还是VAR方法,都需要面对覆盖全局方法、全局变量的危险,解决这样的情况,我们可以生成一个唯一的函数名或者变量名来尽可能的防止出现这样的情况,例:

 
  
  1. functionName = "yundanran" + new Date().getTime();
  2. varName = "yundanran" + new Date().getTime();

这样的重复的概率就大大降低了。

第二个问题是,如何扩展该方法,两种方法大都雷同,可以合二为一。综合例:

 
  
  1. /**
  2. * 跨域读取
  3. * @param {String} 跨域方法,jsonp或var
  4. * @param {String} 跨域地址
  5. * @param {Function} 跨域成功回调
  6. * @param {Function} 跨域失败回调
  7. * @return {Undefined} 无返回
  8. * @author 云淡然
  9. * @version 1.0
  10. * 2013年11月20日14:30:51
  11. */
  12. function crossDomain(type, url, onsuccess, onerror) {
  13. // 设置回调为
  14. var callbackName = "prefix" + new Date().getTime() + "callback";
  15. // 创建回调函数
  16. if (type == "jsonp") {
  17. window[callbackName] = function () {
  18. if (onsuccess) onsuccess(arguments[0]);
  19. }
  20. }
  21. // 创建一个 script 的 DOM 对象
  22. script = document.createElement("script");
  23. // 设置其同步属性
  24. script.async = true;
  25. // 设置其地址
  26. script.src = url.replace(/#.*$/, "") + (/\?/.test(url) ? "&" : "?") + type + "=" + callbackName;
  27. // 监听
  28. script.onload = script.onreadystatechange = function () {
  29. if (!script.readyState || /loaded|complete/.test(script.readyState)) {
  30. script.onload = script.onreadystatechange = null;
  31. if (type == "var") {
  32. if (onsuccess) onsuccess(window[callbackName]);
  33. }
  34. // 移除该 script 的 DOM 对象
  35. if (script.parentNode) {
  36. script.parentNode.removeChild(script);
  37. }
  38. // 删除函数或变量
  39. window[callbackName] = null;
  40. }
  41. }
  42. script.onerror = function () {
  43. if (onerror) onerror();
  44. }
  45. // 插入head
  46. head.appendChild(script);
  47. }

1.1

CROS(Cross-Origin Resource Sharing)跨域资源共享,定义了必须在访问跨域资源时,浏览器与服务器应该如何沟通。CROS背后的基本思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。

复制代码代码如下:


以上的trigkit4是相对路径,如果我们要使用CORS,相关Ajax代码可能如下所示:

复制代码代码如下:


代码与之前的区别就在于相对路径换成了其他域的绝对路径,也就是你要跨域访问的接口地址。

服务器端对于CORS的支持,主要就是通过设置Access-Control-Allow-Origin来进行的。如果浏览器检测到相应的设置,就可以允许Ajax进行跨域的访问。


jquery会自动生成一个全局函数来替换callback=?中的问号,之后获取到数据后又会自动销毁,实际上就是起一个临时代理函数的作用。$.getJSON方法会自动判断是否跨域,不跨域的话,就调用普通的ajax方法;跨域的话,则会以异步加载js文件的形式来调用jsonp的回调函数。

JSONP的优缺点

JSONP的优点是:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果。

JSONP的缺点则是:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。

CROS和JSONP对比

CORS与JSONP相比,无疑更为先进、方便和可靠。

    1、 JSONP只能实现GET请求,而CORS支持所有类型的HTTP请求。

    2、 使用CORS,开发者可以使用普通的XMLHttpRequest发起请求和获得数据,比起JSONP有更好的错误处理。

    3、 JSONP主要被老的浏览器支持,它们往往不支持CORS,而绝大多数现代浏览器都已经支持了CORS)。




4、总结

研究了几天,虽然对多种跨域方法都有所了解了,但是真要投入应用还是明显不够的(还是需要借助一些js库)。

每种方法都有其优缺点,使用的时候其实应该将多种跨域方法进一步封装一下,统一调用的接口,利用js来自动判断哪种方法更为适用 。


 

你可能感兴趣的:(解决ajax跨域的方法原理详解)