深入剖析jsonp跨域原理


在項目中遇到一個jsonp跨域的問題,於是仔細的研究了一番jsonp跨域的原理。搞明白了一些以前不是很懂的地方,比如:

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

2)jsonp跨域的原理到底是什么;

3)除了jsonp跨域之外還有那些方法繞過“同源策略”,實現跨域訪問;

4)jsonp和ajax,或者說jsonp和XMLHttpRequest是什么關系;

等等。

1.同源策略

說到跨域,首先要明白“同源策略”。同源是指:js腳本只能訪問或者請求相同協議,相同domain(網址/ip),相同端口的頁面。

我們知道,js腳本可以訪問所在頁面的所有元素。通過ajax技術,js也可以訪問同一協議,同一個domain(ip),同一端口的服務器上的其他頁面,請求到瀏覽器端之后,利用js就可以進行任意的訪問。但是對於協議不同, 或者domain不同或者端口不同的服務器上的頁面就無能為力了,完全不能進行請求。

下面在本地搭建兩個tomcat,分別將端口設為8080,和8888,進行相關實驗。顯然他們的端口是不同的。演示如下:

http://localhost:8888/html4/ajax.html的代碼如下:

<!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腳本到達瀏覽器會執行的特性,我們可以進行跨域取得數據。我們用一個例子來說明:

8888端口的html4項目中的jsonp.html頁面代碼如下:

<!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代碼如下:

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 啦^--^

明白了原理之后,我們再看一個更加實用的例子:

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來如下的跨域訪問他:

<!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 參數注釋掉,都可以跨域成功:

$(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自動生成的一個函數名稱,其對應源碼如下:

var oldCallbacks = [],
	rjsonp = /(=)\?(?=&|$)|\?\?/;

// Default jsonp settings
jQuery.ajaxSetup({
	jsonp: "callback",
	jsonpCallback: function() {
		var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
		this[ callback ] = true;
		return callback;
	}
});

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM