JSONP. Salvando la limitación de dominio de XHR cuando se hace Cross-Scripting (XSS)

Existe una variación de JSON llamada JSONP (P de padding – con relleno-) que permite salvar uno de los problemas que existe en las llamadas AJAX a través del objeto XMLHttpRequest. Esta limitación consiste en que desde la página web no podemos escribir un Javascript que llame a otro dominio. Esto es así para evitar problemas de seguridad.

Por ejemplo, en uno de los post anteriores en el que se probaba la utilidad de JSON con Jquery se hacía una llamada asíncrona desde JQuery a un servlet en el mismo servidor que respondía con datos en formato JSON. Parte del código de la llamada era

$.post("../jqueryJSON.action", {parametroJSON:$.toJSON(person)} ,

El parámetro de la URL de llamada es ../jqueryJSON.action, que es una dirección relativa y está en el propio servidor, que en este caso es localhost.
Esta vez colocaré la dirección de forma explícita (haz acto de fe que es la buena):

<pre>$.post("http://localhost:8084/TwitterPrueba1/jqueryJSON.action", {parametroJSON:$.toJSON(person)}</pre>

La página sigue haciendo peticiones sin problemas porque su URL es: http://localhost:8084/TwitterPrueba1/jqueryExamples/jqueryJSON.html

Ahora imaginemos que el servidor está en otra URL. Como estoy trabajando en local, voy a hacer un truco, que es cambiar localhost por 127.0.0.1. Todos sabemos que son equivalentes, pero a nivel de dominio son diferentes. Entonces tengo ahora

$.post("http://localhost:8084/TwitterPrueba1/jqueryJSON.action", {parametroJSON:$.toJSON(person)}

Recargo la página y si utilizo una consola de Javascript como firebug de ffox o la de Chrome, tengo el siguiente error al hacer la petición.

XMLHttpRequest cannot load http://127.0.0.1:8084/TwitterPrueba1/jqueryJSON.action/jqueryJSON.action. Origin http://localhost:8084 is not allowed by Access-Control-Allow-Origin.

Como se puede comprobar, no es posible hacer una llamada asíncrona a una url fuera del dominio

Por aclarar, se está usando Jquery, que enmascara todo el proceso de uso del objeto XMLHttpRequest, pero internamente se usa.

¿Para qué queremos cargar información de otros servidores? Muchas de las compañías de servicios de Internet publican sus API a través de este formato (Twitter,Flicker, Google, Yahoo…), de modo que podemos utilizar la potencia de sus servicios fácilmente.

Para poder hacer llamadas a otros servidores y cargar la información de otros servicios, se puede utilizar JSONP, que realmente lo que hace es, en vez de hacer una llamada con el objeto XMLHttpRequest (XHR), imita la inserción de un script en nuestra página, que contendrá básicamente la información de vuelta enformato JSON y la llamada a un función de Javascript que hemos creado.

Por tanto lo que tiene que devolver el servidor es algo del tipo

funcion(Respuesta_formato_JSON)

Este texto como respuesta se meterá dentro de un tag script, de modo que se podrá ejecutar sin llamar al objeto XHR. Por tanto en el cliente, se incluye algo parecido a esto:

<script src="URL_que_devuelve_JSONP"></scrip>

Esa URL como digo devolverá texto en formato JSONP que no es otra cosa que JSON “envuelto” entre paréntesis como parámetro de una función.

Entonces, si vamos más allá y vemos cómo se comporta el escript con la URL, quedará algo así:

<script>funcion(datos_JSONP)</script>

Este código será ejecutado normalmente por el navegador como cualquier otro Javascript

¿Cómo se especifica el nombre de la función que “envuelve” el JSON de respuesta?

Sencillamente se añadirá un parámetro más a la llamada de la URL, por ejemplo “callback”. En este parámetro se le indicará al servlet cómo se llama la función del cliente.

Por usar un servicio conocido que emplee JSONP, el buscador Yahoo provee los resultados en este formato:

http://search.yahooapis.com/ImageSearchService/V1/imageSearch?appid=YahooDemo&output=json&query=pepe&callback=MIFUNCION

Como se puede comprobar, devuelve los respuesta a la búsqueda en Yahoo “pepe” en formato JSON, incluyendo además la función MIFUNCION envolviendo toda la información.

Por lo tanto, en el lado del servidor se debe preparar para devolver la respuesta incluyendo esta función, cuyo nombre proporciona el cliente en Javascript a través de un parámetro.

Modificando el ejemplo de Struts2 en el que se usa Gson, simplemente sería añadir un nuevo parámetro cuyo valor se inyecta, siempre que tengamos el setter preparado:

private String callback;   //Nuevo parámetro

public void setCallback(String callback) {
	this.callback = callback;
}

El otro cambio que hay que hacer es la salida, envolviendo la cadena de salida en formato JSON con el nombre de la función que ha indicado el cliente:

salidaFormatoJSON = callback + "(" + salidaFormatoJSON + ")";

El resultado final de la acción de Struts2 que sirve el código en formato JSONP usando Gson es:

imports...

public class jqueryJSONP extends ActionSupport implements
        ServletRequestAware, ServletResponseAware {

    private HttpServletRequest request;
    private HttpServletResponse response;

    private String parametroJSON;
    private String callback;   //Nuevo parámetro

    public jqueryJSONP() {
    }

    public String execute() throws Exception {

        System.out.println("Parámetro de llamada:" + parametroJSON);

        Gson gson = new Gson();
        ServletOutputStream sos= response.getOutputStream();

        Persona persona=gson.fromJson(parametroJSON, Persona.class);
        System.out.println("Persona Recibida. Nombre:" + persona.nombre + ";Edad:" + persona.edad);

        //1. Conversión de un ArrayList de Objetos a JSON
        ArrayList prueba1Array = new ArrayList();
        for (int i = 0; i &lt; persona.edad; i++) {
            prueba1Array.add(new Persona("Nombre_" + i, ((int)(Math.random()*100))));
        }
        String salidaFormatoJSON = gson.toJson(prueba1Array);

        salidaFormatoJSON = callback + "(" + salidaFormatoJSON + ")";
        sos.print(salidaFormatoJSON);
        System.out.println("Salida:" + salidaFormatoJSON);

        return null;

    }

    public void setServletRequest(HttpServletRequest hsr) {
        this.request = hsr;
    }

    public void setServletResponse(HttpServletResponse hsr) {
        this.response = hsr;
    }

    public String getParametroJSON() {
        return parametroJSON;
    }

    public void setParametroJSON(String parametroJSON) {
        this.parametroJSON = parametroJSON;
    }

    public void setCallback(String callback) {
        this.callback = callback;
    }

    //Inner class
    private static class Persona {

        String nombre;
        int edad;

        //Necesario para el método fromJson
        public Persona() {
        }

        public Persona(String n, int e) {
            nombre = n;
            edad = e;
        }
    }
}

De este modo hemos creado una acción en Struts2 que puede ser llamada desde cualquier dominio usando JSONP. Es importante tener en cuenta que cuando publicamos algo con JSONP es accesible por cualquier cliente, de modo que debe ser información NO SENSIBLE.

Ahora toca el trabajo en el lado del cliente. Vamos a utilizar el ejemplo modificado que se empleó en el post de explicación de Json desde Jquery un poco modificado.

La llamada al servidor deberá ser de tipo GET, ya que no es una llamadd XHR, sino que es una llamada para hacer un include. Más información en http://www.markhneedham.com/blog/2009/08/27/jquery-post-jsonp-and-cross-domain-requests/

Por tanto vamos utilizar la función $.get de jQuery para recoger la información en formato JSONP con el añadido de la llamada a la función. El procedimiento es muy parecido, sólo que en esta ocasión se construirá la URL incluyendo los parámetros, tal y como se hace con Get

En este caso el código Javascript envía al servidor un objeto persona, con nombre y edad. El servidor devolverá otros objetos persona, tantos como edad tenga el individuo indicado. Por ejemplo 10 años, devolverá como respuesta en JSONP un array con 10 individuos.

Los parámetros en la llamada de la función $.getJSON (o $.get, ya que sólo evita poner el tipo “json” al final), debe constuirse en la URL, ya que de otro modo no funciona.

Esto es incorrecto

$.getJSON("http://localhost:8084/TwitterPrueba1/jqueryJSONP.action", {parametroJSON:$.toJSON(person),callback:"?"} ,

La URL debe construirse de forma explícita, concatenando cadenas:

$.getJSON("http://localhost:8084/TwitterPrueba1/jqueryJSONP.action?parametroJSON="+ $.toJSON(person)+"&callback=?" ,

Si se han seguido las indicaciones anteriores, se esperará que el parámetro callback tenga el nombre de la función de vuelta que tiene que incluir el servidor en la respuesta JSONP. La explicación de que sea una interrogación “?” es que JQuery sustituye la interrogación por un valor determinado por él, y será la referencia a la función de resultado que tiene $getJSON.

A continuación se muestra un ejemplo de salida del servidor en formato JSONP. Obsérverse el nombre elegido por jQuery para la función de callback = jsonp1290095678230

jsonp1290095678230([{"nombre":"Nombre_0","edad":31},{"nombre":"Nombre_1","edad":52},{"nombre":"Nombre_2","edad":82},{"nombre":"Nombre_3","edad":19},{"nombre":"Nombre_4","edad":14},{"nombre":"Nombre_5","edad":33},{"nombre":"Nombre_6","edad":86},{"nombre":"Nombre_7","edad":70},{"nombre":"Nombre_8","edad":56},{"nombre":"Nombre_9","edad":37}])

El resto del proceso no difiere de una llamada JSON normal y corriente. Se ejecutará la función de respuesta, que es otro parámetro de getJSON. En este caso esta función rellena, para mostrar un ejemplo, un select con opciones basadas en las “personas” obtenidas, borrando las que hubiera con anterioridad.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <title></title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <script language="javascript" src="jquery/jquery.js"></script>
        <script language="javascript" src="jquery/jquery.json-2.2.js"></script>

        <script language="javascript">
            $(document).ready(function() {

                //Petición AJAX, que envía dos parámetros y recibe la respuesta con JSON
                $("#envioPeticion").click(function(e){
                    e.preventDefault();

                    //Crea el objeto que se va a enviar
                    person = new persona($("#nombre").val(),$("#edad").val());

                    $.getJSON("http://localhost:8084/TwitterPrueba1/jqueryJSONP.action?parametroJSON="+ $.toJSON(person)+"&amp;callback=?" ,
                    function(response){

                        console.log("no hay response");

                        var i = 0;
                        var textoSalida = "";

                        console.log(response);
                        

                        $("#selector> option").remove();


                        for (i=0;i<response.length;i++)
                        {
                            textoSalida = textoSalida + response[i].nombre + " - " + response[i].edad;
                            textoSalida = textoSalida + "<br/>";
                            
                            //Añade además al selector los valores
                            $('<option/>').attr("value",response[i].edad).text(response[i].nombre).appendTo("#selector");
                        }
                        //$("#vueltaServidor").html(textoSalida);    //Pega lo recibido en el div
                    });
                });

             

                //Objeto persona
                function persona (nombre, edad)
                {
                    this.nombre = nombre;
                    this.edad = parseInt(edad);
                }

            });
        </script>
    </head>
    <body>

        <h1>Prueba de JSONP</h1>


        <form>
            <input type ="text" id ="nombre" value ="Luis" />
            <input type ="text" id ="edad" value ="10"/>
            <a href="#" id="envioPeticion">Envía la petición</a>
        </form>

        <hr />
        <h3>Esta es la vuelta del servidor</h3>
        <p>Se recibe un objeto JSON y se dibuja en la pantalla</p>

        <form>
            <select id="selector">
                <option value="pepe">pepe</option>
                <option value="luis">luis</option>
            </select>
        </form>

        <div id ="vueltaServidor" style=""></div>


    </body>
</html>

Mediante la utilización de JSONP se podrá acceder a la información que proveen otros servidores, no solamente el servidor en el que se encuentra la página. Existen multitud de servicios que proveen de datos mediante este sistema, como por ejemplo Yahoo o Twitter.

Algunos enlaces útiles:

Anuncios

One comment

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s