JSONP, CORS y como los soportamos desde NodeJS
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.
- Same Origin Policy
- JSONP
- Soportando JSONP desde NodeJS
- CORS
- Soportando CORS desde NodeJS
- 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:
- Protocolo
- Host (Dominio/Dirección de IP)
- 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:
- https://localhost:3000/algo – Diferente protocolo
- http://localhost:1100/algo – Diferente puerto
- http://localhost/algo – Diferente puerto (el 80)
- http://www.google.com/algo – Diferente host
- http://sub.localhost:3000/algo – Diferente host (debe ser exacto)
- 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:
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:
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:
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
Respuesta de JSON con Padding
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:
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):
Ese script va a generar la llamada a la función llamame enviándole el JSON:
Retorno del servicio
NOTA: con jquery se puede evitar la vuelta de funciones, simplificarlo un poco.
El ejemplo anterior con jQuery quedaría algo asi:
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:
-
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.
-
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:
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:
El servidor va a comprobar la propiedad Origen y decide si permite el acceso, una respuesta satisfactoria seria:
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):