天天看点

跨越访问之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 应用程序的安全威胁 

继续阅读