前言:
今天,無意間看到自己某個文件夾下有個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代碼:

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如下:

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如下:

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,因為也沒有實際遇到過,不知道什么更好的解決方案。今天就到這里了,希望對大家有用~