RSS Feed

JSONP, CORS y como los soportamos desde NodeJS

7

septiembre 17, 2012 by - @pjnovas

Laburando con mi API de feriados me crucé con el tema de Same Origin Policy, digamos que una API Rest de feriados debería tener soporte para ser llamada desde un js en el cliente mínimamente, esto me llevo a conocer cosas nuevas y quería compartir mi experiencia.

  1. Same Origin Policy
  2. JSONP
  3. Soportando JSONP desde NodeJS
  4. CORS
  5. Soportando CORS desde NodeJS
  6. Links Útiles

Same Origin Policy

El principal problema que nos encontramos al querer hacer una llamada desde un dominio a otro desde un javascript en el cliente (por ejemplo, una llamada ajax) es lo que se conoce como “Same Origin Policy” (política de mismo origen), es decir, una seguridad impuesta para evitar las llamadas entre distintos orígenes desde client-side.

Ahora, como identificamos que estamos en otro origen?
Este Origen está definido por:

  1. Protocolo
  2. Host (Dominio/Dirección de IP)
  3. Puerto

Esto significa que si alguno de los anteriores no es igual, es otro origen, y por esta seguridad, no podemos realizar la llamada ajax, hacer un GET de un .json, o cualquier operación desde un Explorador.

Por ejemplo: Supongamos que estamos en http://localhost:3000 e intentamos acceder a:

  1. https://localhost:3000/algo – Diferente protocolo
  2. http://localhost:1100/algo – Diferente puerto
  3. http://localhost/algo – Diferente puerto (el 80)
  4. http://www.google.com/algo – Diferente host
  5. http://sub.localhost:3000/algo – Diferente host (debe ser exacto)
  6. http://localhost:3000/algo – Correcto

En todos los casos anteriores (menos el último) vamos a recibir un error, ya que no estamos cumpliendo con la seguridad.
Bueno, ahora sabemos el por qué del error, cómo hacemos para llamar a otro Origen desde el cliente?

JSONP

Para arrancar, la sigla viene de JSON + P, lo que sería Javascript Object Notation with (con) Padding. Ese Padding es un complemento para el JSON, y para que queremos ese complemento?, supongamos el siguiente escenario:

Tenemos un script de cliente en el que queremos llamar a un servicio que retorna un JSON por AJAX, supongamos que el servicio es en NoLaborables:

Por ejemplo, el Próximo feriado http://nolaborables.com.ar/API/v1/proximo y nos devuelve un JSON:

{
  "dia": 24,
  "mes": 9,
  "motivo": "Bicentenario de la Batalla de Tucumán",
  "tipo": "nolaborable"
}

Este servicio está en otro dominio, y ahora sabemos que tenemos la política Same Origin y no podemos hacer una llamada AJAX.

Y si referenciamos una url bajo un tag script?, por ejemplo, esto si podemos hacerlo:

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"></script>

Eso si funciona, ya que no hay problema en referenciar a un script que esté en otro origen, ahí no juega la política de Same Origin.
Entonces referenciemos al servicio con un script:

<script type="text/javascript" src="http://nolaborables.com.ar/API/v1/proximo"></script>

Error!, porque el interpretador de JS no sabe como leer un json colgado de la nada, pero si la respuesta de ese servicio nos devolviera el json como una llamada a una función?, el interpretador sabe que hacer con eso, no?

Respuesta normal de JSON
{
  "dia": 24,
  "mes": 9,
  "motivo": "Bicentenario de la Batalla de Tucumán",
  "tipo": "nolaborable"
}
Respuesta de JSON con Padding
miFunction({
  "dia": 24,
  "mes": 9,
  "motivo": "Bicentenario de la Batalla de Tucumán",
  "tipo": "nolaborable"
});

Eso es el padding, el server nos responde el JSON como una llamada a una función, para que podamos referenciarlo por un script y así saltearnos la política de Same Origin.

Genial, o sea que para implementarme la llamada completa tendría que hacer algo asi:


function llamame(jsonRespuesta){
  //hago algo con el JSON del server: jsonRespuesta
}

Después inyectamos el siguiente script para que haga la llamada, indicándole cual es la función a la que va a llamar (el Padding):

<script type="text/javascript" src="http://nolaborables.com.ar/API/v1/proximo?callback=llamame"></script>

Ese script va a generar la llamada a la función llamame enviándole el JSON:

Retorno del servicio
llamame({
  "dia": 24,
  "mes": 9,
  "motivo": "Bicentenario de la Batalla de Tucumán",
  "tipo": "nolaborable"
});

NOTA: con jquery se puede evitar la vuelta de funciones, simplificarlo un poco.
El ejemplo anterior con jQuery quedaría algo asi:

$.ajax({
  url: "http://nolaborables.com.ar/API/v1/proximo",
  dataType: 'jsonp'
}).done(function(jsonRespuesta) { 
  console.dir(jsonRespuesta);
});

En este caso jQuery se encarga de armar el script, con la función de vuelta y nos entrega el resultado en el parámetro “jsonRespuesta” de forma transparente, sólo tenemos que especificar que el tipo va a ser ‘JSONP’.

Listo, ya nos despreocupamos de la Política de Mismo Origen, pero hay dos temas nuevos a tener en cuenta:

1. Seguridad: pensemos que con esto estamos inyectando un script en nuestra página directo desde un servidor (que en algunos casos no es nuestro), por lo que si el servidor tiene ganas de inyectar otra cosa, va a correr sin problemas en nuestro sitio, por lo que tenemos que confiar mucho en el servicio para hacer una llamada JSONP.

2. El servidor tiene que soportar esta llamada: tiene que estar preparado para que le puedas pedir JSONP y en ese caso retornarte el JSON con su Padding, sino, vamos a seguir recibiendo el JSON pelado y no nos sirve.

Soportando JSONP desde NodeJS

Para soportarlo en NodeJS de forma manual, deberiamos leer el request, comprobar si en el header nos pide JSONP como dataType y retornar el Padding con el JSON de respuesta. Pero ya que existen web frameworks, y en muy pocos casos tendríamos un servidor web http a mano, vamos a hacerlo con Express?:

En la configuración de express sólo especificamos que soporte callbacks JSONP:

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.set("jsonp callback", true);
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(express.cookieParser());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

Listo, con esa linea soportamos JSONP con Express :)

Estuve leyendo por ahí preguntas sobre hacer un POST con JSONP, ahora que entendemos como funciona, podemos entender el “porque” es imposible realizar un POST: si estamos realizando un GET desde un tag script, como que no tenemos forma de cambiar el método HTTP.

CORS

Desde que existe JSONP hay muchas críticas del tema y convengamos que es un “work-around” al Same Origin Policy, hoy por hoy tenemos otra salida a este problema: CORS (Cross-Origin Resource Sharing).
El objetivo de CORS es que bajo una propiedad en el header de la respuesta HTTP se puedan definir los origenes que pueden acceder al servidor como Cross Domain.

La cosa ahora se pone mucho mas simple, desde el cliente no tenemos que hacer nada especial, el punto es que habilitamos en el servidor para que pueda ser llamado desde otro origen.
Esto lo hacemos agregando una nueva propiedad en el header del HTTP request, es decir, en el pedido (request) al servidor especificamos que origenes están permitidos.

Veamos un ejemplo:

Cuando disparamos una llamada Ajax a un servidor, el explorador se encarga de agregar a nuestro header http la propiedad Origin con el valor de nuestro origen, por ejemplo:

GET /API/v1/proximo HTTP/1.1 <- metodo HTTP con el path al que llamanos
Host: nolaborables.com.ar <- host al que estamos llamando
User-Agent: Mozilla, Chrome, etc..
Accept: text/html,application/json
Connection: keep-alive
Origin: http://midominio.com  <- aca estamos nosotros

El servidor va a comprobar la propiedad Origen y decide si permite el acceso, una respuesta satisfactoria seria:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *  <- aca tenemos la propiedad que dice si se puede o no
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/json

Como vemos en la respuesta (response) del servidor, nos devuelve la propiedad Access-Control-Allow-Origin donde nos especifica que origen puede acceder, en el caso anterior es un * asi qué cualquier origen pasa tranquilo.

En esa propiedad también podemos especificar sólo algunos origines, también se puede filtrar por métodos HTTP, por ejemplo, solo darle acceso mediante GET.

Soportando CORS desde NodeJS

Como vimos, es una propiedad en el header, asi que de nuevo, si estamos creando un web server a mano, es agregar esa propiedad. Te dejo una forma de hacerlo con Express creando un middleware (sería como un método que se llama para toda request que ocurra):

function perimitirCrossDomain(req, res, next) {
  //en vez de * se puede definir SÓLO los orígenes que permitimos
  res.header('Access-Control-Allow-Origin', '*'); 
  //metodos http permitidos para CORS
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); 
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  next();
}

//Siguiendo con la configuración de Express, agregamos el middleware
app.configure(function() {
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(express.cookieParser());
  app.use(perimitirCrossDomain);
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

Links útiles

Same Origin Policy

Cross-Origin Resource Sharing


  • matiasarriola

    Excelente post! cada vez mejor vienen :D
    El soporte para CORS no está tan mal http://caniuse.com/cors

  • Nicolas

    Espectacular post! tuve que lidiar con la same origin policy hace un tiempo. El fernet cada vez esta saliendo mejor!

  • Pingback: » Login con Facebook en Symfony Proyecto Fusa

  • Javier

    Muy buen articulo!! me resolvió un dolor de cabeza de varias horas

  • PerePere

    Genial!! Me salvaste. :D

  • ¿Recomendaciones?

    Hola, tuve que hacer esto para que funcionara el ejemplo:

    $.ajax({

    url: “http://nolaborables.info/API/v1/proximo”,

    dataType: ‘jsonp’,

    jsonpCallback: “llamame”,

    }).done(function llamame(data){

    console.dir(data);

    });

    No se si el ejemplo este mal o yo estaba poniendo algo erróneo.

    • pjnovas

      Buenas, puede que sea un tema de version de jquery o browser?.

      Te dejo un jsfiddle donde funciona http://jsfiddle.net/pjnovas/cfALV/

      * de paso te aviso por si tenes planeado usar nolaborables que paso a .com.ar

      Saludos!