今天,無心間看到本身某個文件夾下有個JSONP的東西。慢慢回憶起,這個東西是以前想寫的一個demo,也不知道是多久之前了,可是不知道怎麼的,給忘那邊了。那麼,就趁這個機會把它完成吧,其實也說不上是一個demo,就是一個小實驗,雖然,網上也已經有不少關於JSONP的文章和例子了,可是有些東西看看很簡單,不親自試一下總以爲不踏實。我今天爲何要實驗,一方面也是常常在網上看到有些網站須要跨域得到數據,可是目前本身作的項目中又沒有相關需求,因而很好奇,因而就有了這篇文章,因而......那就開始此次練習吧。javascript
JSONP全稱:JSON with Padding php
看到名字,好像說,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又是如何作到的呢?html
JSONP能夠實現跨域,這要歸功於強大的<script></script>元素標籤。除了咱們會在它中間寫js代碼外,也常常會在網站中經過它的src屬性引入外部js文件,關鍵就在此,咱們的引入的js文件也能夠不是同一個域下的。那麼咱們也就能夠將原來須要獲取的JSON數據寫到js文件中去,而後再獲取。不過,不幸的事情終究發生了,當咱們把一段JSON格式的數據,例如:java
1 {"id" : "1","name" : "小王"}
寫入js文件,而後經過<script>元素引入後,卻報錯了。緣由是<script>標籤元素仍是很老實的,由於它就是負責執行js的,因此你那個JSON格式的數據它也會堅決果斷的看成js代碼去執行,而那個數據根本不符合js語法,因而就很悲劇的出錯了。但這個出錯,一樣卻帶給了咱們答案,不是嗎?既然不符合js語法不行,咱們搞個符合的不就能夠了。這裏一種經常使用的辦法就是返回一個函數callback({"id" : "1","name" : "小王"}); 的執行語句就能夠了。這裏的callback命名不是必須的,你能夠換任何喜歡的名字。這裏只是強調這是個回調函數才這麼寫。回調函數確實強大啊,要使得這裏能夠執行該函數,那麼這個函數必須在開始就已經被咱們提早定義了。咱們在開始就定義好:jquery
function callback(data){ alert(data.name); }
其實這個不難理解,普通的函數執行或許你們都明白,在<script>標籤中間先定義上面的函數,可是該函數並不會運行,由於你沒執行調用,當你接着在代碼中寫上
callback({"id" : "1","name" : "小王"});就順利的執行了。而JSONP所作的就是這個事情,只不過調用的語句從遠程服務器傳來,動態加入到你的頁面中去執行而已。到這裏只剩下最後一步了,就是告訴服務器端返回哪一個名稱的函數執行,這個也好辦,將函數名以一個查詢參數傳遞到後臺告訴它名字就行了,相似:ajax
http://www.b.com/getUsers.json?callback=getUsersjson
而後在服務器端處理,得到參數callback的值,而後將數據填充到getUser(data);的函數參數中去,這裏的data。返回前臺頁面後,即可以執行並得到data數據了。到此,也終於明白了JSON with Padding中的Padding(填充)了。關於JSONP的基礎理論部分就結束了,剩下的內容就剩下實驗部分了。vim
開始了,這裏選擇Node.js,沒其它緣由,我只是順手抓到它了,你固然也能夠用asp.net,java servlet,php,ruby,Golang等等等你喜歡的去實驗。由於只實驗JSONP,沒多少東西,因此Node.js中也沒有使用第三方的框架(不事後來有點後悔了,多寫了好多......)。windows
首先須要模擬兩個域,由於我在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是很是方便的,只須要修改最開始的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可能引發的安全問題主要是可能會遭受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,由於也沒有實際遇到過,不知道什麼更好的解決方案。今天就到這裏了,但願對你們有用~