jsonp跨域原理

在项目中遇到一个jsonp跨域的问题,因而仔细的研究了一番jsonp跨域的原理。搞明白了一些之前不是很懂的地方,好比:javascript

1)jsonp跨域只能是get请求,而不能是post请求;css

2)jsonp跨域的原理究竟是什么;html

3)除了jsonp跨域以外还有那些方法绕过“同源策略”,实现跨域访问;html5

4)jsonp和ajax,或者说jsonp和XMLHttpRequest是什么关系;java

   虽然 jsonp 的实现跟 ajax 没有半毛钱关系jsonp是经过 script的src实现的(具体看后面的解析),可是最终目的都是向服务器请求数据而后回调,并且为了方便,因此      jQuery把 jsonp 也封装在了 $.ajax 方法中,调用方式与 ajax 调用方式略有区别。jquery

等等。ajax

1.同源策略json

说到跨域,首先要明白“同源策略”。同源是指:js脚本只能访问或者请求相同协议相同域名(网址/ip),相同端口的页面。跨域

咱们知道,js脚本能够访问所在页面的全部元素。经过ajax技术,js也能够访问同一协议,同一个domain(ip),同一端口的服务器上的其余页面,请求到浏览器端以后,利用js就能够进行任意的访问。可是对于协议不一样, 或者domain不一样或者端口不一样的服务器上的页面就无能为力了,彻底不能进行请求。浏览器

下面在本地搭建两个tomcat,分别将端口设为8080,和8888,进行相关实验。显然他们的端口是不一样的。演示以下:

http://localhost:8888/html4/ajax.html的代码以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<! doctype  html>
< html >
< head >
     < meta  charset="utf-8">
     < meta  name="keywords" content="jsonp">
     < meta  name="description" content="jsonp">
     < title >jsonp</ title >
     < style  type="text/css">
         *{margin:0;padding:0;}
         a{display:inline-block;margin:50px 50px;}
     </ style >
</ head >
< body >
     < a  href="javascript:;" onclick="myAjax();">click me</ a >
     
< script  type="text/javascript" src="js/jquery-1.11.1.min.js"></ script >
< script  type="text/javascript">
function myAjax(){
     var xmlhttp;
     if(window.XMLHttpRequest){
         xmlhttp = new XMLHttpRequest();
     }else{
         xmlhttp = ActionXObject("Microsoft.XMLHTTP");
     }
     
     xmlhttp.onreadystatechange = function(){
         if (xmlhttp.readyState==4 && xmlhttp.status==200){
             console.log(xmlhttp.responseText);         
         }
     }
     var url = "http://localhost:8080/minisns/json.jsp" + "?r=" + Math.random();
     xmlhttp.open("Get", url, true);
     xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
 
     xmlhttp.send();
}
 
</ script >
</ body >
</ html >

这里为告终果不受其余js库的干扰,使用了原生的XMLHttpRequest来处理,结果以下:

咱们看到8080端口的js的ajax请求没法访问8888端口的页面。缘由是“同源策略不容许读取”。

既然普通的ajax不能访问,那么怎样才能访问呢?你们都知道,使用jsonp啊,那jsonp的原理是什么呢?他为何能跨域呢?

2.jsonp跨域的原理

咱们知道,在页面上有三种资源是能够与页面自己不一样源的。它们是:js脚本,css样式文件,图片,像taobao等大型网站,很定会将这些静态资源放入cdn中,而后在页面上链接,以下所示,因此它们是能够连接访问到不一样源的资源的。

1)<script type="text/javascript" src="某个cdn地址" ></script>

2)<link type="text/css" rel="stylesheet" href="某个cdn地址" />

3)<img src="某个cdn地址" alt=""/>

而jsonp就是利用了<script>标签能够连接到不一样源的js脚本,来到达跨域目的。当连接的资源到达浏览器时,浏览器会根据他们的类型来采起不一样的处理方式,好比,若是是css文件,则会进行对页面 repaint,若是是img 则会将图片渲染出来,若是是script 脚本,则会进行执行,好比咱们在页面引入了jquery库,为何就可使用 $ 了呢?就是由于 jquery 库被浏览器执行以后,会给全局对象window增长一个属性: $ ,因此咱们才能使用 $ 来进行各类处理。(另外为何要通常要加css放在头部,而js脚本放在body尾部呢,就是为了减小repaint的次数,另外由于js引擎是单线程执行,若是将js脚本放在头部,那么在js引擎在执行js代码时,会形成页面暂停。)

利用 页面上 script 标签能够跨域,而且其 src 指定的js脚本到达浏览器会执行的特性,咱们能够进行跨域取得数据。咱们用一个例子来讲明:

(1)访问js

 

8888端口的html4项目中的jsonp.html页面代码以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<! doctype  html>
< html >
< head >
     < meta  charset="utf-8">
     < meta  name="keywords" content="jsonp">
     < meta  name="description" content="jsonp">
     < title >jsonp</ title >
</ head >
< body >
 
< script  type="text/javascript" src="js/jquery-1.11.1.js"></ script >
< script  type="text/javascript">
var url = "http://localhost:8080/html5/jsonp_data.js";
// 建立script标签,设置其属性
var script = document.createElement('script');
script.setAttribute('src', url);
// 把script标签加入head,此时调用开始
document.getElementsByTagName('head')[0].appendChild(script);
function callbackFun(data)
{
     console.log(data.age);
     console.log(data.name);
}  
</ script >
</ body >
</ html >

 其访问的8080端口的html5项目中的jsonp_data.js代码以下:

1
callbackFun({ "age" :100, "name" : "yuanfang" })

 将两个tomcate启动,用浏览器访问8888端口的html4项目中的jsonp.html,结果以下:

上面咱们看到,咱们从8888 端口的页面经过 script 标签成功 的访问到了8080 端口下的jsonp_data.js中的数据。这就是 jsonp 的基本原理,利用script标签的特性,将数据使用json格式用一个函数包裹起来,而后在进行访问的页面中定义一个相同函数名的函数,由于 script 标签src引用的js脚本到达浏览器时会执行,而咱们有定义了一个同名的函数,因此json格式的数据,就作完参数传递给了咱们定义的同名函数了。这样就完成了跨域数据交换。jsonp的含义是:json with padding,而在json数据外包裹它的那个函数,就是所谓的 padding 啦^--^

 

(2)访问servlet---比较实用的例子

8080端口的html5项目中定义一个servlet:

复制代码
package com.tz.servlet;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.alibaba.fastjson.JSON;

@WebServlet("/JsonServlet")
public class JsonServlet extends HttpServlet 
{
    private static final long serialVersionUID = 4335775212856826743L;

    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException 
    {
        String callbackfun = request.getParameter("mycallback");
        System.out.println(callbackfun);    // callbackFun
        response.setContentType("text/json;charset=utf-8");
        
        User user = new User();
        user.setName("yuanfang");
        user.setAge(100);
        Object obj = JSON.toJSON(user);
        
        System.out.println(user);            // com.tz.servlet.User@164ff87
        System.out.println(obj);            // {"age":100,"name":"yuanfang"}
        callbackfun += "(" + obj + ")";    
        System.out.println(callbackfun);    // callbackFun({"age":100,"name":"yuanfang"})
        
        response.getWriter().println(callbackfun);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException 
    {
        this.doPost(request, response);
    }

}
复制代码

 

 在8888端口的html4项目中的jsonp.html来以下的跨域访问他:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<! doctype  html>
< html >
< head >
     < meta  charset="utf-8">
     < meta  name="keywords" content="jsonp">
     < meta  name="description" content="jsonp">
     < title >jsonp</ title >
     < style  type="text/css">
         *{margin:0;padding:0;}
         div{width:600px;height:100px;margin:20px auto;}
     </ style >
</ head >
< body >
     < div >
         < a  href="javascript:;">jsonp测试</ a >
     </ div >
     
< script  type="text/javascript" src="js/jquery-1.11.1.js"></ script >
< script  type="text/javascript">
function callbackFun(data)
{
     console.log(111);
     console.log(data.name);
     //data.age = 10000000;
     //alert(0000);
}
$(function(){
     $("a").on("click", function(){     
         $.ajax({
             type:"post",
             url:"http://localhost:8080/html5/JsonServlet",
             dataType:'jsonp',
             jsonp:'mycallback',
             jsonpCallback:'callbackFun',
             success:function(data) {
                 console.log(2222);
                 console.log(data.age);
             }
         });
     })
});
</ script >
</ body >
</ html >

 结果以下:

咱们看到,咱们成功的跨域取到了servlet中的数据,并且在咱们指定的回调函数jsonpCallback:'callbackFun' 和 sucess 指定的回调函数中都进行了执行。并且老是callbackFun先执行,若是咱们打开注释://data.age = 10000000; //alert(0000);

就会发现:在callbackFun中对 data 进行修改以后,success指定的回调函数的结果也会发生变化,并且经过alert(0000),咱们肯定了若是alert(000)没有执行完,success指定的函数就不会开始执行,就是说两个回调函数是前后同步执行的。

结果以下:

3.jsonp 跨域与 ajax  

从上面的介绍和例子,咱们知道了 jsonp 跨域的原理,是利用了script标签的特性来进行的,可是这和ajax有什么关系呢?显然script标签加载js脚本和ajax一点关系都没有,在没有ajax技术以前,script标签就存在了的。只不过是jquery的封装,使用了ajax来向服务器传递 jsonp 和 jsonpCallback 这两个参数而已。若是咱们再服务器端和客户端,对参数 jsonp 和 jsonpCallback 的值,协调好(也就是一致性),那么就没有必要使用ajax来传递着两个参数了,就像上面第二个例子那样,直接构造一个script标签就好了。不过实际上,咱们仍是会使用ajax的封装,由于它在调用完成以后,又将动态添加的script标签去掉了,咱们看下相关的源码:

 

上面的代码先构造一个script标签,而后注册一个onload的回调,最后将构造好的script标签insert进去。insert完成以后,会触发onload回调,其中又将前面插入的script标签去掉了。其中的 代码 callback( 200, "success" ) 其实就是触发 ajax 的jsonp成功时的success回调函数,callback函数实际上是一个 done 函数,其中包含了下面的代码:

 

由于传入的是 200 ,因此 isSuccess = true; 因此执行 "success"中的回调函数,response = ajaxHandleResponse(...) 就是咱们处理服务器servelt返回的数据,咱们能够调试:console.log(response.data.age); console.log(response.data.name); 看到结果。

3.jsonp 跨域与 get/post 

咱们知道 script,link, img 等等标签引入外部资源,都是 get 请求的,那么就决定了 jsonp 必定是 get 的,那么为何咱们上面的代码中使用的 post 请求也成功了呢?这是由于当咱们指定dataType:'jsonp',不论你指定:type:"post" 或者type:"get",其实质上进行的都是 get 请求!!!从两个方面能够证实这一点:

1)若是咱们将JsonServlet中的 doGet()方法注释掉,那么上面的跨域访问就不能进行,或者在 doPost() 和 doGet() 方法中进行调试,均可以证实这一点;

2)咱们看下firebug中的“网络”选项卡:

咱们看到,即便咱们指定 type:"post",当dataType:"jsonp" 时,进行的也是 GET请求,而不是post请求,也就是说jsonp时,type参数始终是"get",而不论咱们指定他的值是什么,jquery在里面将它设定为了get. 咱们甚至能够将 type 参数注释掉,均可以跨域成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$( function (){
     $( "a" ).on( "click" function (){     
         $.ajax({
             //type:"post",
             url: "http://localhost:8080/html5/JsonServlet" ,
             dataType: 'jsonp' ,
             jsonp: 'mycallback' ,
             jsonpCallback: 'callbackFun' ,
             success: function (data) {
                 console.log(2222);
                 console.log(data.age);
             }
         });
     })
});

 

因此jsonp跨域只能是get,jquery在封装jsonp跨域时,不论咱们指定的是get仍是post,他统一换成了get请求,估计这样能够减小错误吧。其对应的query源码以下所示:

复制代码
// Handle cache's special case and global
jQuery.ajaxPrefilter( "script", function( s ) {
    if ( s.cache === undefined ) {
        s.cache = false;
    }
    if ( s.crossDomain ) {
        s.type = "GET";
        s.global = false;
    }
});
复制代码

if( s.crossDomain){ s.type = "GET"; ...} 这里就是真相!!!!!!!!在ajax的过滤函数中,只要是跨域,jquery就将其type设置成"GET",真是那句话:在源码面前,一切了无秘密!jquery源码我本身不少地方读不懂,可是并不妨碍咱们去读,去探索!

 

4.除了jsonp跨域方法以外的其余跨域方法

其实除了jsonp跨域以外,还有其余方法绕过同源策略,

1)由于同源策略是针对客户端的,在服务器端没有什么同源策略,是能够随便访问的,因此咱们能够经过下面的方法绕过客户端的同源策略的限制:客户端先访问 同源的服务端代码,该同源的服务端代码,使用httpclient等方法,再去访问不一样源的 服务端代码,而后将结果返回给客户端,这样就间接实现了跨域。相关例子,参见博文:http://www.cnblogs.com/digdeep/p/4198643.html

2)在服务端开启cors也能够支持浏览器的跨域访问。cors即:Cross-Origin Resource Sharing 跨域资源共享。jsonp和cors的区别是jsonp几乎全部浏览器都支持,可是只能是get,而cors有些老浏览器不支持,可是get/post都支持,cors的支持状况,能够参见下图(来自:http://caniuse.com/#search=cors)

cors实例:

项目html5中的Cors servlet:

复制代码
public class Cors extends HttpServlet 
{
    private static final long serialVersionUID = 1L;
       

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException 
    {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type");
        response.getWriter().write("cors get");
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
    {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type");
        response.getWriter().write("cors post");
    }

}
复制代码

在html4项目中访问他:

复制代码
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="keywords" content="jsonp">
    <meta name="description" content="jsonp">
    <title>cors</title>
    <style type="text/css">
        *{margin:0;padding:0;}
        div{width:600px;height:100px;margin:20px auto;}
    </style>
</head>
<body>
    <div>
        <a href="javascript:;">cors测试</a>
    </div>
    
<script type="text/javascript" src="js/jquery-1.11.1.js"></script>
<script type="text/javascript">
$(function(){
    $("a").on("click", function(){        
        $.ajax({
            type:"post",
            url:"http://localhost:8080/html5/cors",
            success:function(data) {
                console.log(data);
                alert(data);
            }
        });
    })
});    
</script>
</body>
</html>
复制代码

访问结果以下:

5. 参数jsonp 和 jsonpCallback

jsonp指定使用哪一个名字将回调函数传给服务端,也就是在服务端经过 request.getParameter(""); 的那个名字,而jsonpCallback就是request.getParamete("")取得的值,也就是回调函数的名称。其实这两个参数均可以不指定,只要咱们是经过 success : 来指定回调函数的状况下,就能够省略这两个参数,jsnop若是不知道,默认是 "callback",jsnpCallback不指定,是jquery自动生成的一个函数名称,其对应源码以下:

1
2
3
4
5
6
7
8
9
10
11
12
var  oldCallbacks = [],
     rjsonp = /(=)\?(?=&|$)|\?\?/;
 
// Default jsonp settings
jQuery.ajaxSetup({
     jsonp:  "callback" ,
     jsonpCallback:  function () {
         var  callback = oldCallbacks.pop() || ( jQuery.expando +  "_"  + ( nonce++ ) );
         this [ callback ] =  true ;
         return  callback;
     }
});
相关文章
相关标签/搜索