天天看點

跨越通路之JSONP實踐

預備知識:

        1.什麼是JSONP:JSONP(JSON with Padding)是一個非官方的協定,它允許在伺服器端內建Script tags傳回至用戶端,通過javascript callback的形式實作跨域通路(這僅僅是JSONP簡單的實作形式)。

        2.為什麼使用JSONP:由于 JSON 隻是一種含有簡單括号結構的純文字,是以許多通道都可以交換 JSON 消息。因為 同源政策的限制,我們不能在與外部伺服器進行通信的時候使用 XMLHttpRequest。而JSONP是一種可以繞過同源政策的方法,即通過使用 JSON 與 <script> 标記相結合的方法,從服務端直接傳回可執行的JavaScript函數調用或者JavaScript對象。

        3.誰在使用JSONP: dojo、 JQuery、 Youtube GData API 、 Google Social Graph API 、 Digg API 、 GeoNames webservice、 豆瓣API、 Del.icio.us JSON API等。

如何使用JSONP:

        在這裡我使用Restlet來講講JSONP的實踐體驗。之是以使用Restlet,是因為它非常易于開發、并可深入協定内部進行細粒度開發。

        首先,我們介紹一下會用到的代碼元件:Restlet1.1M2、Prototype1.6、JSON官方庫。Restlet本身提供了JSON擴充庫(org.restlet.ext.json.JsonRepresentation),但其需要JSON官方庫提供支援,是以仍然需要自己下載下傳并引入JSON官方庫。

建立Application:

import ...

public class JSONReprApplication {
        public static void main(String[] args) throws Exception{
                // Create a component
                Component component = new Component();
                component.getServers().add(Protocol.HTTP, 8182);
                component.getClients().add(Protocol.FILE);
                
                Application application=new Application(component.getContext()){
                        @Override
                        public Restlet createRoot(){
                                //directory where static web files live
                                final String DIR_ROOT_URI="file:///E:/eclipse3.1RC3/workspace/RestletPractice/static_files/";
                                //create router
                                Router router=new Router(getContext());
                                //attach static web files to "www" folder
                                Directory dir=new Directory(getContext(),DIR_ROOT_URI);
                                dir.setListingAllowed(true);
                                dir.setDeeplyAccessible(true);
                                dir.setNegotiateContent(true);
                                router.attach("/www/",dir);
                                //attach restlet
                                router.attach("/json/people/{user}",new JSONReprRestlet());
                                router.attach("/json/people/{user}/jsonp/{callback}",new JSONReprRestlet());
                                return router;
                        }
                };
                // Attach the application to the component and start it
                component.getDefaultHost().attach("",application);
                component.start();
        }
}
      

        在上面的代碼中,我們在Application中添加了靜态檔案服務、Restlet:靜态檔案服務是将指定的目錄(DIR_ROOT_URI)挂接在“www”虛拟目錄上,而JSONReprRestlet被挂接在“/json/people/{user}”和“/json/people/{user}/jsonp/{callback}”這兩個URL模闆上。關于如何編寫Restlet應用程式的詳細說明請參考: 《Restlet指南》。

編寫Restlet:

import ...

public class JSONReprRestlet extends Restlet{
        @Override
    public void handle(Request request, Response response) {
                //load information
                if (request.getMethod()==Method.GET){
                        JSONObject defaultPeopleJObj=new JSONObject(
                                        new People(
                                                        (String)request.getAttributes().get("user"),
                                                        1,
                                                        "咔咔"
                                        )
                                );
                        if (request.getAttributes().get("user")!=null && request.getAttributes().get("callback")==null){
                                JsonRepresentation jsonRepr=new JsonRepresentation(defaultPeopleJObj);
                                jsonRepr.setCharacterSet(CharacterSet.UTF_8);
                                jsonRepr.setMediaType(MediaType.TEXT_PLAIN);
                                response.setEntity(jsonRepr);
                        }
                        //jsonp
                        if (request.getAttributes().get("user")!=null && request.getAttributes().get("callback")!=null){
                                String callbackFunc=(String)request.getAttributes().get("callback");
                                JsonRepresentation jsonRepr=new JsonRepresentation(callbackFunc+"("+defaultPeopleJObj.toString()+");");
                                jsonRepr.setCharacterSet(CharacterSet.UTF_8);
                                jsonRepr.setMediaType(MediaType.APPLICATION_JAVASCRIPT);
                                response.setEntity(jsonRepr);
                        }
                }
                //update information
                else if (request.getMethod()==Method.POST){
                        Form form=request.getEntityAsForm();
                        String jsonStr=form.getFirstValue("json");
                        JSONObject updatedPeopleJObj=null;
                        try {
                                updatedPeopleJObj=new JSONObject(jsonStr);
                                updatedPeopleJObj.put("name",(String)request.getAttributes().get("user"));
                        } catch (JSONException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                        }
                        JsonRepresentation jsonRepr=new JsonRepresentation(updatedPeopleJObj);
                        jsonRepr.setCharacterSet(CharacterSet.UTF_8);
                        jsonRepr.setMediaType(MediaType.TEXT_PLAIN);
                        response.setEntity(jsonRepr);
                }
                
        }
}
      

        上面代碼針對GET和POST兩種方法分别進行處理:

        在處理GET方法時将其是為讀取People資訊的請求,而request屬性中的“user”參數值便是我們前面在Application中設定URL模闆裡面的{user}在request的URL中的實際對應值,後面的“callback”參數同理。當request中隻有“user”參數,則傳回名字為“user”參數值的people對象的JSON表示法。而如果request中還包括了“callback”參數,這說明此request為一個“JSONP”的請求,此時将“callback”參數值作為JavaScript函數名,并将名字為“user”參數值的people對象的JSON字元串作為函數的唯一參數,進而将兩者構造成了一個JavaScript函數調用:callback函數(json字元串)。也許這裡了解起來有些抽象,但當你把實踐中用到JSONP後便會明白。

       在處理POST方法時将其是為更新People資訊的請求,是以我們需要在頁面中通過JSON的方式送出更新後的people給服務端,這在後面的html代碼片段中将會看到。而這裡的處理方式非常簡單:将JSON字元串解析為對象,然後再以JSON表示法傳回給用戶端。

頁面代碼片段:

<script type=/"text/javascript/" src=/"./javascript/prototype-1.6.0.2.js/"></script>
<title>JSON擴充測試頁</title>
</head>
<body>
<div id=/"appScope/">
        <div id=/"people/">
                <p>
                        <label id=/"name/"></label>(<label id=/"age/"></label>)正在說:/"<label id=/"talking/"></label>/"
                </p>
                <p>
                        年齡:<input type=/"text/" name=/"newAge/" id=/"newAge/" value=/"/"/>
                        話語:<input type=/"text/" name=/"newTalking/" id=/"newTalking/" value=/"/"/>
                        <input type=/"button/" name=/"setButton/" id=/"setButton/" value=/"修改/"/>
                </p>
        </div>
</div>
</body>
<script type=/"text/javascript/" src=/"./javascript/jsonExtAppInterface.js/"></script>
<script type=/"text/javascript/" src=/"/json/people/cleverpig/jsonp/refreshPeopleInfo/"></script>
      

JavaScript代碼片段:

/**
 * 重新整理資訊
 */
function refreshPeopleInfo(people){
           $('name').innerHTML=people.name;
           $('age').innerHTML=people.age;
           $('talking').innerHTML=people.talking;
}
/**
 * 設定資訊(年齡、話語)
 */
function setPeopleInfo(){
        var newPeople=new Object();
        newPeople.age=$F('newAge');
        newPeople.talking=$F('newTalking');
        var xmlHttp = new Ajax.Request(
                "/json/people/cleverpig",
                {
                        method: 'post', 
                    parameters: 'json='+encodeURIComponent(Object.toJSON(newPeople)),
                    onComplete: function(transport){
                            var retObj=transport.responseText.evalJSON();
                            $('name').innerHTML=retObj.name;
                            $('age').innerHTML=retObj.age;
                            $('talking').innerHTML=retObj.talking;
                    }
                }
        );
}
Event.observe(
        'setButton',
        'click',
        setPeopleInfo,
        false
);
      

        這裡簡單的功能非常明确:在頁面裝載時調用服務端URL(/json/people/cleverpig/jsonp/refreshPeopleInfo)生成的腳本來重新整理people這個Div裡面的資料項,而點選“修改”按鈕便會将我們輸入的新屬性post到服務端,并接收服務端傳回的JSON字元串更新頁面顯示。

運作截屏:

跨越通路之JSONP實踐

JSONP的安全問題:

        我們可以使用若幹種方法在 JavaScript 程式中動态地生成代碼。最著名的函數之一就是 eval() 函數,該函數允許您将任意字元串做為 JavaScript 代碼執行。然而,肆無忌憚地使用該函數是非常危險的。遺憾的是,一些使用廣泛的 JavaScript 庫在内部直接使用 eval() 函數。

        由于 JSON 是以 JavaScript 的一個子集為基礎的,是以腳本内容會潛在地包含惡意代碼。然而,JSON 是JavaScript 的一個安全的子集,不含有指派和調用。是以,許多 JavaScript 庫使用 eval() 函數将 JSON 轉換成 JavaScript 對象。要利用這點,攻擊者可以向這些庫發送畸形的 JSON 對象,這樣 eval() 函數就會執行這些惡意代碼。可以采取一些方法來保護 JSON 的使用。這裡提供一種方法:使用 RFC 4627 中所定義的正規表達式確定 JSON 資料中不包含活動的部分。

        下面 示範了如何使用正規表達式檢查 JSON 字元串:

var my_JSON_object = !(/[^,:{}/[/]0-9./-+Eaeflnr-u /n/r/t]/.test(
            text.replace(/"(//.|[^"//])*"/g, '' ''))) &&
            eval(''('' + text + '')'');
      

源代碼下載下傳:

jsonp_source.rar

參考資源:

Restlet指南

Restlet讨論組

Implement JSONP in your Asp.net Application

Allen Wang對JSONP的介紹

GWT Tutorial: How to Read Web Services Client-Side with JSONP

JSON for jQuery

征服 Ajax 應用程式的安全威脅 

繼續閱讀