Hola a todos, hoy vuelvo de nuevo al ataque con REST. Vamos a empezar a diseñar cosas un poco más complejas. En el anterior post sobre REST vimos sobre todo como diseñar servicios REST que representaran el patrón colección/entidad. Me gustaría ahora centrarme en un punto importante: el modelado de operaciones de negocio.
Como sabéis, en REST, al contrario que en OO tradicional, no podemos extender de forma arbitraria el conjunto de operaciones que podemos ejecutar sobre un recurso u objeto. Así pues, ¿qué hacemos para modelar operaciones de negocio arbitrarias? Supongamos por ejemplo que tengo una tienda de libros, y tengo mi recurso REST que representa a mis libros, ¿cómo modelo la operación de vender un libro? Si estuviéramos en OO tradicional podríamos añadir un método vender(Cliente,Numero de unidades) a la interfaz de un supuesto objeto Libro, o quizás tuviéramos una interfaz IGestionLibreria donde añadir un método equivalente. Pero en REST no podemos añadir métodos, en concreto si usamos HTTP y el patrón colección/entidad, sólo tenemos las operaciones CRUD.
En estos casos la forma correcta de modelar la operación es aplicar el patrón comando. Para los que no conozcáis el patrón comando, éste consiste en transformar una operación en un objeto. En vez de crear un método «vender», creamos una clase «Vender». Cada vez que queramos ejecutar una operación «Vender», instanciamos dicha clase e invocamos su método «execute». En el constructor de ésta clase pasamos los parámetros que necesita la operación, a saber, el libro, el cliente y cuantas unidades queremos vender. Lo interesante del patrón comando es que podemos almacenar las instancias de las operaciones que hemos ejecutados, y mantener de esta manera un histórico de operaciones. Si a la clase que representa la operación le añadimos una operación «undo», podemos usar dicha operación y el histórico de comandos para implementar una cola de undo/redo, cosa bastante interesante.
¿Cómo aplicamos el patrón comando al mundo REST? Sencillo, cada vez que tengamos una operación de negocio que no encaje directamente en un CRUD sobre los recursos existentes, creamos un nuevo recurso que represente la ejecución de dicha operación. A esto, en algunos sitios, lo llaman proceso de reificación, yo simplemente lo llamaré el patrón comando aplicado a REST.
El proceso para ejecutar una venta, sería realizar una petición de creación de una entidad «venta» sobre la colección de ventas. Siguiendo el patrón colección/entidad:
- Petición, nótense los parámetros en el cuerpo. Para variar esta vez no usaré JSON:
POST https://www.myshop.com/store/orders HTTP/1.1 Content-Type: application/x-www-form-urlencoded Accept: application/x-www-form-urlencoded;application/json;charset=UTF-8;q=0.5 bookId=Q1T52GVE3&customerId=juan34&numberOfBooks=1
- Respuesta ok.
HTTP/1.1 201 Created Location: https://www.myshop.com/store/orders/RE356CWX
Esta es la forma más sencilla de implementar un comando en REST, hacemos un POST a la colección pertinente y pasamos los parámetros en el body. El sistema ejecuta el comando, lo registra en un histórico y nos devuelve la URI de la ejecución del comando. En los casos más sencillos, este histórico no necesita ser persistente y puede durar sólo durante la sesión de usuario, o durante un tiempo predeterminado. En otros casos nos interesa un histórico persistente, normalmente por motivos de auditoria y depuración.
Ahora pensemos un poco en la dura realidad, ¿qué ocurre si el servidor no me responde? Esto puede pasar por múltiples motivos: perdimos la conectividad, el mensaje de respuesta se perdió, el navegador se colgó, la respuesta tarda en llegar porque usamos un super ADSL de oferta, etc. Además el protocolo HTTP no asegura la entrega de mensajes. Ante este tipo de errores lo normal es reintentar, ¿cuántas veces hemos refrescado el navegador? Esto no es problema cuando usamos GET, PUT o DELETE, ya que son idempotentes. Pero hemos usado POST para modelar la ejecución del comando, si repetimos la petición, y resultó que la anterior se había realizado con éxito, pero nosotros no nos habíamos enterado, el comando se ejecutará por duplicado. En casos simples esto no importa, y la solución anterior es adecuada. En la mayoría de los casos esto no es así. No queremos que un cliente impaciente que no para de pulsar el botón de comprar reciba en casa veinte ejemplares de «Como se hizo te lo dije…».
La solución en este caso consiste en separar la ejecución del comando en dos fases: crear el comando en una petición y ejecutarlo en otra. Veámoslo:
- Petición de crear comando. Esta vez no enviamos parámetros, no es necesario con este diseño, pero podríamos si quisiéramos.
POST https://www.myshop.com/store/orders HTTP/1.1 Accept: application/x-www-form-urlencoded;application/json;charset=UTF-8;q=0.5
- Respuesta ok.
HTTP/1.1 201 Created Location: https://www.myshop.com/store/orders/RE356CWX
- Rellenamos el comando con un PUT, mandando los parámetros en el body.
PUT https://www.myshop.com/store/orders/RE356CWX HTTP/1.1 Content-Type: application/x-www-form-urlencoded Accept: application/x-www-form-urlencoded;application/json;charset=UTF-8;q=0.5 bookId=Q1T52GVE3&customerId=juan34&numberOfBooks=1
- Respuesta de parámetros actualizados con éxito.
HTTP/1.1 204 No Content
- Finalmente ejecutamos el comando con otro PUT. Fijaos en el campo «execute» que lo ponemos a true. Asumimos que si un parámetro no está en la petición, simplemente no se modifica.
PUT https://www.myshop.com/store/orders/RE356CWX HTTP/1.1 Content-Type: application/x-www-form-urlencoded Accept: application/x-www-form-urlencoded;application/json;charset=UTF-8;q=0.5 execute=true
- Respuesta de ejecución correcta:
HTTP/1.1 204 No Content
Como vemos, este diseño nos ofrece mayor flexibilidad. Por un lado podemos repetir el paso 3 cuantas veces queramos, y así construir el comando poco a poco o cambiar de opinión sobre cuantos ejemplares de un libro queremos comprar. Este enfoque encaja muy bien con interfaces de usuario RIA en las que el usuario va «faciendo entuertos», que si compra, que si ahora no, etc. También podemos fusionar el paso 3 y el 5 en una única petición PUT, que contenga tanto los parámetros como el campo execute. Por ejemplo, el caso de que ahora el usuario quiera 5 ejemplares, y queramos ejecutar directamente el comando:
PUT https://www.myshop.com/store/orders/RE356CWX HTTP/1.1 Content-Type: application/x-www-form-urlencoded Accept: application/x-www-form-urlencoded;application/json;charset=UTF-8;q=0.5 bookId=Q1T52GVE3&customerId=juan34&numberOfBooks=5&execute=true
Por supuesto siempre podemos hacer un GET sobre la URI del comando para saber si está ejecutado o no y los parámetros con los que está configurado.
Otro punto interesante es que podemos extender este diseño del patrón comando, sustituyendo el campo binario «execute», por un campo «state» que admita varios valores. Si usamos el campo binario «execute» el comando o se ha ejecutado o no, pero a veces necesitamos poder representar estados de ejecución intermedios. Por ejemplo, un pedido a la tienda de libros, puede pasar por varios estados: «nuevo» cuando se ha creado pero no se ha formalizado, «pendiente» cuando hemos hecho la compra, «enviado» cuando todos los libros están disponibles y han sido enviados a nuestra dirección, «cerrado» cuando hemos recibido los libros y «cancelado» si nos arrepentimos. La idea es que el comando puede pasar de un estado a otro, aunque todas las transiciones no tienen por que ser válidas, y cada transición de un estado a otro puede tener efectos secundarios, como que me cobren cuando paso de «nuevo» a «pendiente» o que me devuelvan el dinero si paso a «cancelado». Que curioso, cambiando un campo binario, por un campo que admite varios estados, acabamos de modelar la interfaz REST de un BPM, transformar un simple comando en un proceso de negocio, ¿alguien da más? Que sea fácil implementar toda esta lógica en el servidor es otra cosa, pero desde el punto de vista del consumidor del recurso REST, la cosa está bastante sencilla.
¿Cómo implementamos una funcionalidad de undo/redo? En el primer enfoque, más simplista, basta con mandar una petición DELETE a la URI del comando que se ha creado. En el segundo enfoque podemos usar también DELETE, pero tenemos la opción de hacer un PUT mandando el campo «execute» a false. De la misma manera que el comando se ejecuta al cambiar «execute» de false a true, se produce una operación de deshacer cuando cambiamos dicho campo de true a false. La ventaja sobre usar DELETE es que no perdéis los datos que contenía el comando, con lo que podéis hacer un «redo». Si tenemos un proceso, en vez de un comando, no tiene mucho sentido usar esta metáfora de «undo/redo». Simplemente añadimos estados y transiciones apropiadas entre estos. Por ejemplo, para «deshacer» un pedido simplemente lo pasamos a estado «cancelado». Parece claro que podemos ir al estado «cancelado» desde los estados «nuevo», «pendiente» o «enviado». Si podemos pasar al estado «cancelado» desde el estado «cerrado», o no, depende de la lógica de negocio.
Una última cosa sobre el tema de «undo/redo», hasta ahora, usemos el enfoque de DELETE o el de cambiar el campo «execute», necesitamos la URI del comando. Esto permite al consumidor del cliente hacer «undo» sobre varios comandos en un orden independiente al que fueron ejecutados. Sin embargo, en muchos escenarios, los comandos se deben deshacer en orden inverso al que fueron ejecutados. En estos casos lo más fácil es usar una URI especial, que representa el último comando ejecutado. Por ejemplo: https://www.myshop.com/system/orders/last. Cualquier operación sobre esta URI, nos devuelve una redirección temporal, código HTTP «303 See Other», a la URI del último comando ejecutado.
Veamos ahora que podemos hacer ante errores de comunicación, como los mencionados anteriormente. En caso de no recibir respuesta del servidor, podemos repetir tantas veces como queramos el paso 3 o el 5, ya que PUT es idempotente. ¿Y si falla el POST del paso 1? Pues repetimos el paso 1. El repetir el paso 1 no es idempotente, por lo que en el servidor se crearán comandos vacíos, pero no ejecutados. Esto no es demasiado grave, ya que el comando no ha llegado a ejecutarse, y no le hemos cobrado dinero a nadie. Por otro lado sólo nos interesa persistir comandos que se han ejecutado, con lo que dichos comandos vacíos no necesitan ocupar espacio en disco.
También podemos tener otro problema con esto del POST duplicado, ¿y si ocurre un ataque, en el que un bot se dedica a ejecutar miles o millones de POSTs? En este caso, tenéis que tener cuidado, ya que si no implementais bien el servidor, puede ocurrir que se os ocupe la memoria RAM y se os colapse. Existe una técnica para crear comandos vacíos que no ocupan espacio en memoria. Esta técnica consiste en crear tickets como identificadores de entidad para los comandos. Un ticket es un identificador, generado por el servidor, que no puede ser falsificado, pero que es fácilmente verificable como válido por éste. De esta forma, en el POST, lo que hacemos es generar un ticket, para identificar un supuesto nuevo comando, pero no creamos en memoria ningún tipo de objeto ni persistimos nada. Cuando nos hacen un PUT para actualizar o ejecutar un comando, cogemos su identificador de instancia, y como es un ticket podemos comprobar si es un identificador legítimo o no. Sólo admitimos la ejecución del PUT si el ticket es válido.
¿Cómo implementamos este sistema de tickets? Para implementarlo necesitamos tres ingredientes: generar un UUID, una clave secreta que sólo conozca el servidor, y una hash criptográfica. El ticket se construye: uuid + «:» + HASH(uuid + CLAVE) Para comprobar si un ticket es válido basta con coger la parte que llega hasta el «:», concatenarla con la clave, hacerle el HASH y compararla con la segunda parte del ticket. Veamos algo de pseudocódigo:
- Para generar un ticket:
function generateTicket() { var uuid = uuid.newUUID() var key = "The server secret key" ticket = uuid + ":" + hash_SHA(uuid + ":" + key).toBase64(); return ticket }
- Para validar un ticket:
function isValidTicket(ticket) { var colonIndex=ticket.indexOf(":") if(colonIndex==-1) return false; var uuid = ticket.substring(0, colonIndex); var signature= ticket.substring(colonIndex+1); if(!uuid||!signature) return false; var key = "The server secret key" var trueSignature=hash_SHA(uuid + ":" + key).toBase64() return trueSignature == signature; }
- Para actualizar un pedido:
function updateOrder(RESTRequest req) { if(!req.isPut()) return var uri=req.getURI() var entityId= uri.substring(0, uri.lastIndexOf("/")) if(!entityId||!isValidTicket(entityId)) return; var order=dao.findOrder(entityId) if(!order) order=order.createOrderWithId(entityId); var oldExecute=order.execute; order.update(req.getData); order.save(); if(oldExecute&&!order.execute) undoOrder(order); else if(!oldExecute&&order.execute) doOrder(order); }
Notad que en el POST no se crea ningún objeto, sólo un ticket que se devuelve. El ticket tampoco es almacenado en ningún sitio, ya que podemos verificar su autenticidad sin necesidad de ello. La creación del verdadero objeto, se realiza en el primer PUT recibido, y sólo si se ha hecho PUT sobre una URI que contenga un ticket válido.
Con la capacidad de modelar colecciones, entidades y comandos, ya estais en disposición de diseñar interfaces REST para cualquier problema de negocio. Hasta este punto, podéis publicar la misma funcionalidad que con SOAP, pero a mi modo de ver, de una forma más sencilla y clara. Yo diría que el enfoque REST es más orientado a objetos y el enfoque SOAP es más procedural (publicar un servicio SOAP no se distingue mucho de publicar un servicio COBOL y pasarle una commarea). En próximos posts pienso abordar temas como el autodescubrimiento, por qué no necesitamos nada parecido al WSDL y cómo la distinción entre datos y presentación se vuelve borrosa.
[…] This post was mentioned on Twitter by Enrique Amodeo Rubio, Enrique Amodeo Rubio, Jose Manuel Beas, Babelias, Juan F. Valdés gayo and others. Juan F. Valdés gayo said: RT @eamodeorubio: http://ow.ly/2GJle #rest Te lo dije…Servicios web (4): Diseñando servicios REST (2) […]
Hola Enrique,
Estupenda serie. Sólo quería que me aclarases un par de detalles.
El resultado de la primera llamada a POST https://www.myshop.com/store/orders o «crear comando» (vamos a llamarlo así) devuelve, además del OK o KO de la operación, un ticket, ¿cierto? ¿Tienes alguna estrategia de caducidad de esos tickets?
La otra pregunta es: en un diseño DDD (Domain-Driven Design) + CQRS (Command Query Responsibility Separation) entiendo que podemos decir que los Q(ueries) se pueden implementar fácilmente con GET, pero los C(ommand) parece un poco menos evidente. Luismi Cavallé me comentaba que él estaba mapeando los comandos para hacer unos con PUT, otros con DELETE y otros con POST. Yo, sin enbargo, creo que la cosa se queda en todos los comandos con POST. Lo que nos lleva a cuestionar si lo que estamos haciendo es REST. ¿Qué opinión tienes tú?
Un mojito, digo, un saludo,
JMB
Hola JMB, en principio con el código que he puesto los tickets no caducan. La idea es que puedan usarse una única vez para crear el pedido, en el momento que el pedido se ha creado el ticket se convierte en el entityId del pedido y a partir de entonces sólo se puede usar el ticket para actualizar el pedido.
Si por lo que sea necesitas que el ticket caduque puedes construir el ticket de la siguiente manera:
ticket = UUID+’_’+CREATION_TIMESTAMP+»:»+HASH(UUID+’_’+CREATION_TIMESTAMP+»:»+SERVER_SECRET_KEY)
Así puedes usar el CREATION_TIMESTAP y ver si el ticket es antiguo y caducó. Tendrías que cambiar el código de updateOrder para añadir esta comprabación antes de crear el pedido.
Sobre que verbo debes usar, pues te recomiendo que uses el que sea más similar a lo que quieres hacer desde el punto de vista de semántica. Si vas a modelar operaciones idempotentes sobre colecciones/entidades, tal como comenté en el artículo anterior, usa DELETE para borrar y PUT para update.
Si vas a hacer operaciones no idempotentes, el método POST debe estar presente. Como ves en este artículo me centro en modelar Comandos, usando una combinación de POST y PUT con tickets. Sin embargo yo sólo usaría este enfoque para modelar comandos no idempotentes. Si tienes un comando idempotente, lo mejor es usar PUT, DELETE (o incluso GET si es una consulta).
!Buen post¡
Tendré en cuenta tus ideas cuando me toque diseñar una API REST 🙂
Pero tengo un comentario sobre tu solución de tickets contra un llamado ‘replay attack’. En vez de utilizar un UUID, se debe utilizar un nonce (number used once).
Si el ticket se basa en un UUID, sólo tienes que escuchar una petición válida y ya tienes un ticket válido para todas las peticiones que quieras.
Y utilizar TLS tampoco es una respuesta válida porque ya existen ataques MITM con la ayuda de root-CAs de ciertos países.
http://en.wikipedia.org/wiki/Cryptographic_nonce
http://en.wikipedia.org/wiki/Replay_attack
http://en.wikipedia.org/wiki/Universally_unique_identifier
Hola Thp, sobre el tema del ticket. Si te fijas bien el ticket que propongo es una especia de «nonce» débil, ya que es difícil de falsificar, impredecible y sólo se puede usar una vez para crear un nuevo objeto. No sólo consiste en un UUID, es UUID+:+HASH(UUID+»:»+SECRET). En teoría esto lo hace muy difícil de falsificar y predecir. Si miras el código verás que con dicho ticket sólo se puede crear un único objeto, y no millones. En el sentido de crear un nuevo recurso rest si es un nonce, en el sentido de hacer otras operaciones no. Si alguien captura el ticket puede acceder a dicho objeto y modificarlo, pero sólo si consigue autentificarse como un usuario válido con permisos de escritura sobre dicho objeto, que es otro tema. En definitiva es un problema parecido al de session hijacking donde se captura el token o cookie de sesión. Si te fijas no he cubierto todavía el tema del login y control de acceso.
Lo bonito del REST es que puedes usar esquemas más estrictos, si quieres, con relativa facilidad.
Lo que comentas de los ataques MITM con root-CA de países me preocupa. Desde mi punto de vista eso podría comprometer toda la seguridad basada en PKI, no sólo la de REST, y no veo como evitarlo.
Saludos !
Entiendo lo que dices pero no veo en el código que el ticket «sólo se puede usar una vez para crear un nuevo objeto». Y con UUID:HASH(UUID:SECRET) no hay forma de comprobar si ya fue utilizado o no.
Por un lado (updateOrder()) es correcto lo que dices porque llamas a isValidTicket() con la URL como entityId. Pero la implementación de isValidTicket() no lo refleja.
Habría que poner algo como ENTITYID:UUID:HASH(ENTITYID:UUID:SECRET).
Para confirmar tu preocupación de la debilidad de PKI contra ataques MITM: http://www.eff.org/deeplinks/2010/08/open-letter-verizon
Si te fijas bien en el pseudocódigo de updateOrder, el ticket es el propio entityId. Sólo se puede crear una vez el objeto, la primera vez que atacamos con éxito el método updateOrder con un ticket válido, creamos el pedido si no existiera ya. Si ya se creó con anterioridad no se vuelve a crear. Fíjate en las lineas de la 8 a la 10. En este sentido se evita una duplicación del pedido.
[…] Notad, que en este caso, el enlace “submit” apunta a un comando que usa el patrón ticket descrito en mi anterior artículo. […]
Hola, he estado trabajando con Restful ultimamente pero estoy atascado enviando un application/x-www-form-urlencoded a un @PUT y no reconoce el formulario…
Que estoy haciendo mal?
Espero puedas ayudarme!
Lo primero que deberías comprobar es si tu lógica de servidor admite peticiones PUT. Esto depende mucho de que framework uses. Si usas JAX-RS o servlets simples no habrá problemas. Tienes que comprobar que tu servidor entiende application/x-www-form-urlencoded. Si usas JAX-RS prueba a usar las anotaciones @Consumes(«application/x-www-form-urlencoded»), @Produces(«application/x-www-form-urlencoded») y @FormParam(«aParamName») junto con @PUT
Lo segundo es asegurarte que el lado cliente emita peticiones HTTP con el verbo PUT. Si usas HTML5 no tendrás problemas, sólo debes poner en tu etiqueta
Se me olvidó comentarte, si usas JAX-RS, por defecto sólo soporta «application/x-www-form-urlencoded» si en el servidor usas parámetros y/o valores de retorno de tipo MultivaluedMap
Si quieres usar un bean propio en vez de MultivaluedMap debes registrar un «Entity provider» que lo soporte.
Nosotros hemos empleado la técnica de comandos con éxito, para modelar operaciones como «Alta de Empleado». Pero lo hemos hecho en la modalidad simple, es decir, realizando un POST a recurso altaEmpleados, que ya realiza el alta si toda la información es válida y correcta.
Por otro lado, también hemos usado la técnica de ticketing, pero en nuestro caso para acceder a la descarga de ficheros a través de un pop-up. ¿Por qué? Porque a un pop-up no podemos pasarle información de autenticación, y lo que hacemos es que primero se obtiene un ticket con autenticación, y luego puede acceder a la URI del ticket, que se abre ya en el pop-up, pero esta última no securizada. Si el ticket existe, se le deja pasar, se devuelve el fichero, y por último se borra el ticket, para que no pueda volver a ser consumido.
Otro gran post de Rest, este ya más enfocado al modelado de negocio, que es donde menos información hay actualmente.
[…] https://eamodeorubio.wordpress.com/2010/09/20/servicios-web-4-disenando-servicios-rest-2/ […]
!Felicitaciones por el post Enrique! …Se agradece que compartas conocimiento.
Soy nuevo en esto y quisiera orientación respecto a cuándo debo usar REST. Por ejemplo, ¿es factible para una aplicación web de tipo «punto de venta» que permite gestionar la operación de «n» sucursales? En este tipo de aplicaciones se tratarían recursos con muchos atributos y relaciones complejas.
Si fuera aconsejable usar REST para este caso, ¿cómo se implementaría un mecanismo de autenticación en esta arquitectura para asegurar el consumo de los recursos a usuarios específicos?
Muchas gracias!
Con REST puedes modelar casi cualquier cosa, así que no deberías de tener problema en usarlo para definir la API de un punto de venta.
REST es ideal en el caso de que tengas muchas relaciones complejas, ya que las puedes modelar fácilmente con hyperlinks y forms.
Para seguridad y autenticación puedes usar las cabeceras estándar de HTTP «Authorization» y «Authenticate»
En mi libro puedes encontrar muchos detalles sobre como modelar todo esto: https://leanpub.com/introduccion_apis_rest