在項目中遇到一個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; } });