一次關於JSONP的小實驗與總結


前言:

      今天,無意間看到自己某個文件夾下有個JSONP的東西。慢慢回憶起,這個東西是之前想寫的一個demo,也不知道是多久以前了,但是不知道怎么的,給忘那邊了。那么,就趁這個機會把它完成吧,其實也說不上是一個demo,就是一個小實驗,雖然,網上也已經有很多關於JSONP的文章和例子了,但是有些東西看看很簡單,不親自試一下總覺得不踏實。我今天為什么要實驗,一方面也是經常在網上看到有些網站需要跨域獲得數據,但是目前自己做的項目中又沒有相關需求,於是很好奇,於是就有了這篇文章,於是......那就開始這次練習吧。

一 什么是JSONP

   JSONP全稱:JSON with Padding

   看到名字,好像說,JSONP是JSON的什么?或許有人會問,什么是JSON呢?如果有同學還不清楚JSON,可以先去了解下JSON,然后再繼續本文的閱讀或許會更好。簡單的說,JSON是一種數據交換格式,在我們通過ajax技術獲取數據的時候,可以以XML或者JSON這樣的格式進行傳遞。ajax雖然好用,但是也有遇到困難的時候,比如你需要跨域獲取數據。這個時候,普通的ajax獲取方式就不太容易了,這時候,JSONP就可以幫忙了。這里再補充下前面提到跨域問題,跨域其實簡單的說就是,比如你自己寫了一個網站把它部署到域名是www.a.com的服務器上,然后你可以毫無壓力的使用ajax請求www.a.com/users.json  的數據。 但是,當要你通過普通ajax方式請求www.b.com域名下的www.b.com/users.json的數據時,就沒那么容易了,在后面的小實驗中,可以看到這一情況。既然使用普通的ajax技術無法做到,那么JSONP又是如何做到的呢?

二 JSONP的基本原理

   JSONP可以實現跨域,這要歸功於強大的<script></script>元素標簽。除了我們會在它中間寫js代碼外,也經常會在網站中通過它的src屬性引入外部js文件,關鍵就在此,我們的引入的js文件也可以不是同一個域下的。那么我們也就可以將原來需要獲取的JSON數據寫到js文件中去,然后再獲取。不過,不幸的事情終究發生了,當我們把一段JSON格式的數據,例如:

1 {"id" : "1","name" : "小王"}

寫入js文件,然后通過<script>元素引入后,卻報錯了。原因是<script>標簽元素還是很老實的,因為它就是負責執行js的,所以你那個JSON格式的數據它也會毫不猶豫的當作js代碼去執行,而那個數據根本不符合js語法,於是就很悲劇的出錯了。但這個出錯,同樣卻帶給了我們答案,不是嗎?既然不符合js語法不行,我們搞個符合的不就可以了。這里一種常用的辦法就是返回一個函數callback({"id" : "1","name" : "小王"}); 的執行語句就可以了。這里的callback命名不是必須的,你可以換任何喜歡的名字。這里只是強調這是個回調函數才這么寫。回調函數確實強大啊,要使得這里可以執行該函數,那么這個函數必須在開始就已經被我們提前定義了。我們在開始就定義好:

function callback(data){
    alert(data.name);
}

其實這個不難理解,普通的函數執行或許大家都明白,在<script>標簽中間先定義上面的函數,但是該函數並不會運行,因為你沒執行調用,當你接着在代碼中寫上
callback({"id" : "1","name" : "小王"});就順利的執行了。而JSONP所做的就是這個事情,只不過調用的語句從遠程服務器傳來,動態加入到你的頁面中去執行而已。到這里只剩下最后一步了,就是告訴服務器端返回哪個名稱的函數執行,這個也好辦,將函數名以一個查詢參數傳遞到后台告訴它名字就好了,類似:

http://www.b.com/getUsers.json?callback=getUsers

然后在服務器端處理,獲得參數callback的值,然后將數據填充到getUser(data);的函數參數中去,這里的data。返回前台頁面后,便可以執行並獲得data數據了。到此,也終於明白了JSON with Padding中的Padding(填充)了。關於JSONP的基礎理論部分就結束了,剩下的內容就剩下實驗部分了。

 

三 JSONP小實驗

  •   實驗環境:windows操作系統。
  •   開發工具:NotePad++。
  •   開發語言:Node.js。
  •   前台使用插件:jQuery。

 開始了,這里選擇Node.js,沒其它原因,我只是順手抓到它了,你當然也可以用asp.net,java servlet,php,ruby,Golang等等等你喜歡的去實驗。因為只實驗JSONP,沒多少東西,所以Node.js中也沒有使用第三方的框架(不過后來有點后悔了,多寫了好多......)。

首先需要模擬兩個域,因為我在windows下,所以可以修改host文件,添加

兩個域名映射到本機回送地址127.0.0.1。然后開始寫代碼:

創建兩個Node.js的應用,一個是appA.js,一個appB.js。首先,我們嘗試通過普通ajax獲取同域的數據:

appB.js代碼:

 

View Code
 1 var http = require('http'),
 2       url = require('url'),
 3       fs  = require('fs'),
 4       path = require('path');
 5      
 6 
 7 function getFile(localPath,mimeType,res){
 8     fs.readFile(localPath,function(err,contents){
 9         if(!err){
10             res.writeHead(200,{
11                 'Content-Type' : mimeType,
12                 'Content-length' : contents.length
13             
14             });
15             res.end(contents);
16         }else{
17             res.writeHead(500);
18             res.end();
19         }
20     
21     });
22 }
23 http.createServer(function(req,res){
24     var urlPath = url.parse(req.url).pathname;
25     var fileName = path.basename(req.url) || 'index.html',
26          suffix  = path.extname(fileName).substring(1),
27          dir = path.dirname(req.url).substring(1),
28          localPath = __dirname + '\\';
29     
30 
31     if(suffix === 'js'){
32         localPath += (dir ? dir + '\\' : '') + fileName;
33         path.exists(localPath,function(exists){
34             if(exists){
35                 getFile(localPath,'js',res);
36             }else{
37                 res.writeHead(404);
38                 res.end();
39             }
40             
41         });
42         
43     }else{
44         if(urlPath === '/index'){
45         res.writeHead(200,{'Content-Type':'text/html;charset=utf-8'});
46         var html = '<!DOCTYPE html>'
47                     +'<head>'
48 +'<script type="text/javascript" src="jquery.js"></script>'
49                     
50                     +'<script type="text/javascript" src="http://www.a.com:8088/index?callback=getFollowers"></script>'
51                     +'<script>'
52                     +'$(function(){'
53                     +  ''
54                     +  '$("#getFo").click(function(){'
55                     +'    $.ajax({'
56                     +            'url:"http://www.b.com:9099/followers.json",'
57                     +       'type:"get",'
58                     +       'success:function(json){'
59                     +       '    alert(json.users[0].name);'
60                     +       '}'
61                     +        '});'
62                     +     ''
63                     +    '});'
64                     +'});'
65                     +'</script>'
66                     +'</head>'
67                     +'<body>'
68                     +'<h1>hello i am server b </h1>'
69                     +'<input id="getFo" type="button" value="獲取我的粉絲"/>'
70                     +'</body>'
71                     +'</html>';
72         res.write(html);
73         res.end();
74     }else if(urlPath === '/followers.json'){
75         res.writeHead(200,{'Content-Type':'application/json;charset=utf-8'});
76         var followers = {
77                                 "users" : [
78                                     {"id" : "1","name" : "小王"},
79                                     {"id" : "2","name" : "小李"}
80                                   ]
81                                 };
82         var fjson = JSON.stringify(followers);
83         res.end(fjson);
84     }else{
85         res.writeHead(404,{'Content-Type':'text/html;charset=utf-8'});
86         res.end('page not found');
87     }
88     
89 }
90     
91     
92 }).listen(9099);
93 console.log('Listening app B at 9099...');

 

以上截圖是ajax請求數據部分。我們打開瀏覽器,輸入地址后,如下:

這里有個按鈕獲取我的粉絲,ajax就從url:"http://www.b.com:9099/followers.json該源獲得數據,這個數據在代碼中,我們也可以找到,就是

 當點擊獲取后,如下:

 成功,沒問題,我們再復制一份一樣的代碼,另存為appA.js,然后修改listen端口:

修改appB.js中ajax請求的URL為http://www.a.com:8088/followers.json ,現在是appB服務器本身是http://www.b.com:9099/index 而去請求www.a.com下的數據===》啟動它

然后點擊獲取粉絲按鈕會發現:

 真的沒有取到數據。。。。。。

再試試JSONP的方式,我們修改appB.js如下:

View Code
 1 var http = require('http'),
 2       url = require('url'),
 3       fs  = require('fs'),
 4       path = require('path');
 5      
 6 
 7 function getFile(localPath,mimeType,res){
 8     fs.readFile(localPath,function(err,contents){
 9         if(!err){
10             res.writeHead(200,{
11                 'Content-Type' : mimeType,
12                 'Content-length' : contents.length
13             
14             });
15             res.end(contents);
16         }else{
17             res.writeHead(500);
18             res.end();
19         }
20     
21     });
22 }
23 http.createServer(function(req,res){
24     var urlPath = url.parse(req.url).pathname;
25     var fileName = path.basename(req.url) || 'index.html',
26          suffix  = path.extname(fileName).substring(1),
27          dir = path.dirname(req.url).substring(1),
28          localPath = __dirname + '\\';
29     
30 
31     if(suffix === 'js'){
32         localPath += (dir ? dir + '\\' : '') + fileName;
33         path.exists(localPath,function(exists){
34             if(exists){
35                 getFile(localPath,'js',res);
36             }else{
37                 res.writeHead(404);
38                 res.end();
39             }
40             
41         });
42         
43     }else{
44         if(urlPath === '/index'){
45         res.writeHead(200,{'Content-Type':'text/html;charset=utf-8'});
46         var html = '<!DOCTYPE html>'
47                     +'<head>'
48                     +'<script type="text/javascript">var getFollowers= function(data){alert(decodeURIComponent(data.users[0].name));};</script>'
49                     +'<script type="text/javascript" src="jquery.js"></script>'
50                     
51                     +'<script type="text/javascript" src="http://www.a.com:8088/index?callback=getFollowers"></script>'
52                     +'<script>'
53                     +'$(function(){'
54                     +  ''
55                     +  '$("#getFo").click(function(){'
56                     +'    $.ajax({'
57                     +            'url:"http://www.a.com:8088/followers.json",'
58                     +       'type:"get",'
59                     +       'success:function(json){'
60                     +       '    alert(json.users[0].name);'
61                     +       '}'
62                     +        '});'
63                     +     ''
64                     +    '});'
65                     +'});'
66                     +'</script>'
67                     +'</head>'
68                     +'<body>'
69                     +'<h1>hello i am server b </h1>'
70                     +'<input id="getFo" type="button" value="獲取我的粉絲"/>'
71                     +'</body>'
72                     +'</html>';
73         res.write(html);
74         res.end();
75     }else if(urlPath === '/followers.json'){
76         res.writeHead(200,{'Content-Type':'application/json;charset=utf-8'});
77         var followers = {
78                                 "users" : [
79                                     {"id" : "1","name" : "小王"},
80                                     {"id" : "2","name" : "小李"}
81                                   ]
82                                 };
83         var fjson = JSON.stringify(followers);
84         res.end(fjson);
85     }else{
86         res.writeHead(404,{'Content-Type':'text/html;charset=utf-8'});
87         res.end('page not found');
88     }
89     
90 }
91     
92     
93 }).listen(9099);
94 console.log('Listening app B at 9099...');

 注意看48行和51行,48行定義了回調函數,51行通過<script>標簽,請求不同域的數據,其中傳遞參數callback=getFollowers

然后修改appA.js如下:

View Code
 1 var http = require('http'),
 2      url = require('url'),
 3      querystring = require('querystring');
 4 
 5 http.createServer(function(req,res){
 6     
 7     var path = url.parse(req.url).pathname;
 8     var qs = querystring.parse(req.url.split('?')[1]),
 9          json;
10     if(qs.callback){
11         var followers = {
12                                 users : [{id:'1',name:encodeURIComponent('小王')}]
13                                 };
14         var fjson = JSON.stringify(followers);
15         console.log(fjson);
16         json = qs.callback + "(" + fjson + ");";
17         res.writeHead(200,{
18                         'Content-Type':'application/json',
19                         'Content-Length' : json.length
20                         });
21         res.end(json);
22     
23     }
24     
25     if(path === '/index'){
26         res.writeHead(200,{'Content-Type':'text/html;charset=utf-8'});
27         res.end('home');
28     }else if(path === '/followers.json'){
29         res.writeHead(200,{'Content-Type':'application/json;charset=utf-8'});
30         var followers = {
31                                 "users" : [
32                                     {"id" : "1","name" : "小王"},
33                                     {"id" : "2","name" : "小李"}
34                                   ]
35                                 };
36         var fjson = JSON.stringify(followers);
37         res.end(fjson);
38     }else{
39         res.writeHead(404,{'Content-Type':'text/html;charset=utf-8'});
40         res.end('page not found');
41     }
42     res.end('hello');
43 }).listen(8088);
44 console.log('Listening app A at 8088...');

 第10行-23行,我們處理了傳遞的參數,並將數據填充到函數參數,並發送到請求者那邊。再次運行兩個程序,刷新http://www.b.com:9099/index便直接得到a域下的數據了,似乎成功了。但是,我不想馬上執行呀,我也要和前面一樣,點擊按鈕再獲得,怎么辦?這個也簡單。只需要當我們點擊的時候動態的引入<script>就可以了,修改click事件處理部分的代碼:

1 $("#getFo").click(function(){
2 $("<script><//script>").attr("src","http://www.a.com:8088/index?callback=getFollowers").appendTo("body");
3 });

再次重啟服務器,當點擊按鈕就可以獲取數據了。接下來,我們再看看jQuery又是如何處理JSONP的呢?

 

 

四 jQuery中處理JSONP

  要通過jQuery使用JSONP是非常方便的,只需要修改最開始的ajax部分代碼如下:

1 $.ajax({
2     url:"http://www.a.com:8088/index",
3     dataType:"jsonp",
4     jsonp:"callback",
5     type:"get",
6     success:function(json){
7     alert(decodeURIComponent(json.users[0].name));
8     }
9 });

其中,jsonp指明了querystring的key為callback,value如果不指定,jQuery會默認隨機生成一個名稱:

1 var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( ajax_nonce++ ) );

 

五 JSONP可能引起的安全性問題

  由JSONP可能引起的安全問題主要是可能會遭受CSRF/XSRF的攻擊,而使得容易遭受該攻擊的也恰恰是上文中一直提到的JSONP的特點--可以跨源訪問資源。普通的CSRF/XSRF攻擊,僅僅可能利用受攻擊用戶,騙取服務器的信任,這樣就可以模擬受攻擊者對服務器進行一些有危害的請求,例如修改受攻擊者的個人信息。但是,由於瀏覽器同源策略的限制,在第三方“惡意網站”無法讀取服務器返回的信息。也就是說,攻擊者只能搗搗亂,但是他還是獲取不到受攻擊者的敏感信息的(無XSS注入的前提下)。但是,如果服務器上某個請求使用了JSONP返回用戶數據,可想而知,在第三方,或者任何方網站都能順利的獲取到。關於CSRF/XSRF攻擊,就說到這里,具體實現方式就不展開了。

  除了CSRF/XSRF攻擊外,另外使用JSONP的網站(相對於部署JSONP的服務器)也可能有安全性問題。 因為,通過上面的實驗,我們看到了,通過JSONP請求遠程服務器后,返回的是一個在本網站立即執行的函數。相當於這個腳本直接被注入到當前頁面了。如果遠端網站中存在注入漏洞,那么后果可想而知了。為了防止這樣的事情發生,可以使用 JSON-P 嚴格安全子集使瀏覽器可以對 MIME 類別是“application/json-p”請求做強制處理。如果回應不能被解析為嚴格的 JSON-P,瀏覽器可以丟出一個錯誤或忽略整個回應。關於安全性的問題先說到這里,安全問題永遠是一個矛與盾的問題,總之,在互聯網上,沒有絕對的安全。如果再展開下去又會引出一堆東西,所以今天就先不說了。至於如何防范JSONP容易受到的CSRF/XSRF攻擊,筆者認為最簡單有效的方法就是對於敏感信息不要使用JSONP,因為也沒有實際遇到過,不知道什么更好的解決方案。今天就到這里了,希望對大家有用~


免責聲明!

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



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