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;
     }
});
相關文章
相關標籤/搜索