4. HATEOAS y Seguridad
En esta sesión trataremos uno de los principios REST de obligado cumplimiento para poder hablar de un servicio RESTful. Nos referimos a HATEOAS. Hasta ahora hemos visto cómo los clientes pueden cambiar el estado de los recursos (el nombre del recurso se especifica en la URI de la petición) a través de los contenidos del cuerpo del mensaje, o utilizando parámetros, o cabeceras de petición. A su vez, los servicios comunican el estado resultante de la petición a los clientes a través del contenido del cuerpo del mensaje, códigos de respuesta, y cabeceras de respuesta. Pues bien, teniendo en cuenta lo anterior, HATEOAS hace referencia a que, cuando sea necesario, también deben incluirse los enlaces a los recursos (URI) en el cuerpo de la respuesta (o en las cabeceras), para así poder recuperar el recurso en cuestión, o los recursos relacionados.
En esta sesión también explicaremos algunos conceptos básicos para poder dotar de seguridad a nuestros servicios REST
4.1. ¿Qué es HATEOAS?
Comúnmente se hace referencia a Internet como "la Web" (web significa red, telaraña), debido a que la información está interconectada mediante una serie de hiperenlaces embebidos dentro de los documentos HTML. Estos enlaces crean una especie de "hilos" o "hebras" entre los sitios web relacionados en Internet. Una consecuencia de ello es que los humanos pueden "navegar" por la Web buscando elementos de información relacionados de su interés, haciendo "click" en los diferentes enlaces desde sus navegadores. Los motores de búsqueda pueden "trepar" o "desplazarse" por estos enlaces y crear índices enormes de datos susceptibles de ser "buscados". Sin ellos, Internet no podría tener la propiedad de ser escalable. No habría forma de indexar fácilmente la información, y el registro de sitios web sería un proceso manual bastante tedioso.
Además de los enlaces (links), otra característica fundamental de Internet es HTML. En ocasiones, un sitio web nos solicita que "rellenemos" alguna información para comprar algo o registrarnos en algún servicio. El servidor nos indica a nosotros como clientes qué información necesitamos proporcionar para completar una acción descrita en la página web que estamos viendo. El navegador nos muestra la página web en un formado que podemos entender fácilmente. Nosotros leemos la página web y rellenamos y enviamos el formulario. Un formulario HTML es un formato de datos interesante debido a que auto-describe la interacción entre el cliente y el servidor.
El principio arquitectónico que describe el proceso de enlazado (linking) y el envío de formularios se denomina HATEOAS. Las siglas del término HATEOAS significan Hypermedia As The Engine Of Application State (es decir, el uso de Hipermedia como mecanismo de máquina de estados de la aplicación). La idea de HATEOAS es que el formato de los datos proporciona información extra sobre cómo cambiar el estado de nuestra aplicación. En la Web, los enlaces HTML nos permiten cambiar el estado de nuestro navegador. Por ejemplo cuando estamos leyendo una página web, un enlace nos indica qué posibles documentos (estados) podemos ver a continuación. Cuando hacemos "click" sobre un enlace, el estado del navegador cambia al visitar y mostrar una nueva página web. Los formularios HTML, por otra parte, nos proporcionan una forma de cambiar el estado de un recurso específico de nuestro servidor. Por último, cuando compramos algo en Internet, por ejemplo, estamos creando dos nuevos recursos en el servicio: una transacción con tarjeta de crédito y una orden de compra.
4.2. HATEOAS y Servicios Web
Cuando aplicamos HATEOAS a los servicios web la idea es incluir enlaces en nuestros documentos XML o JSON. La mayoría de las aplicaciones RESTful basadas en XML utilizan el formato Atom Syndication Format para implementar HATEOAS.
4.2.1. Enlaces Atom
Los enlaces Atom constituyen un mecanismo estándar para incluir enlaces (links) en nuestros documentos XML. Veamos un ejemplo:
<clientes>
<link rel="next"
href="http://ejemplo.com/clientes?inicio=2&total=2"
type="application/xml"/>
<cliente id="123">
<nombre>Juan Garcia</nombre>
</cliente>
<cliente id="332">
<nombre>Pablo Bozo</nombre>
</cliente>
</clientes>
El documento anterior representa una lista de clientes, y el elemento <link>
, que contiene un enlace, indica la forma de obtener los siguientes clientes de la lista.
Un enlace Atom es simplemente un elemento XML (elemento <link>
) con unos atributos específicos.
-
El atributo
rel
Se utiliza para indicar la relación del enlace con el elemento XML en el que anidamos dicho enlace. Es el nombre lógico utilizado para referenciar el enlace. Este atributo tiene el mismo significado para la URL que estamos enlazando, que la etiqueta HTML <a> tiene para la URL sobre la que estamos haciendo click con el ratón en el navegador. Si el enlace hace referencia al propio elemento XML en el que incluimos el enlace, entonces asignaremos el valor del atributoself
-
El atributo
href
es la URL a la que podemos acceder para obtener nueva información o cambiar el estado de nuestra aplicación -
El atributo
type
indica el media type asociado con el recurso al que apunta la URL
Cuando un cliente recibe un documento con enlaces Atom, éste busca la relación en la que
está interesado (atributo rel
) e invoca la URI indicada en el atributo href
.
4.2.2. Ventajas de utilizar HATEOAS con Servicios Web
Resulta bastante obvio por qué los enlaces y los formularios tienen mucho que ver en la prevalencia de la Web. Con un navegador, tenemos una "ventana" a todo un mundo de información y servicios. Las máquinas de búsqueda "rastrean" Internet e indexan sitios web para que todos los datos estén al alcance de nuestros "dedos". Esto es posible debido a que la Web es auto-descriptiva. Cuando accedemos a un documento, conocemos cómo recuperar información adicional "siguiendo" los enlaces situados en dicho documento. Por ejemplo, conocemos cómo realizar una compra en Amazon, debido a que los formularios HTML nos indican cómo hacerlo.
Cuando los clientes son "máquinas" en lugar de personas (los servicios Web también se conocen como "Web para máquinas", frente a la "Web para humanos" proporcionada por el acceso a un servidor web a través de un navegador) el tema es algo diferente, puesto que las máquinas no pueden tomar decisiones "sobre la marcha", cosa que los humanos sí pueden hacer. Las máquinas requieren que los programadores les digan cómo interpretar los datos recibidos desde un servicio y cómo realizar transiciones entre estados como resultado de las interacciones entre clientes y servidores.
En este sentido, HATEOAS proporciona algunas ventajas importantes para contribuir a que los clientes sepan cómo utilizar los servicios a la vez que acceden a los mismos. Vamos a comentar algunas de ellas.
Transparencia en la localización
En un sistema RESTful, gracias a HATEOAS, sólo es necesario hacer públicas unas pocas URIs. Los servicios y la información son representados con enlaces que están "embebidos" en los formatos de los datos devueltos por las URIs públicas. Los clientes necesitan conocer los nombres lógicos de los enlaces para "buscar" a través de ellos, pero no necesitan conocer las ubicaciones reales en la red de los servicios a los que acceden.
Los enlaces proporcionan un nivel de indirección, de forma que los servicios subyacentes pueden cambiar sus localizaciones en la red sin alterar la lógica ni el código del cliente.
Desacoplamiento de los detalles de la interacción
Consideremos una petición que nos devuelve una lista de clientes en una base de datos:
GET /clientes
. Si nuestra base de datos tiene miles de datos, probablemente no querremos
devolver todos ellos de una sóla vez. Lo que podemos hacer es definir una vista en
nuestra base de datos utilizando parámetros de consulta, por ejemplo:
/customers?inicio={indiceInicio}&total={numeroElementosDevueltos}
El parámetro inicio
identifica el índice inicial de nuestra lista de clientes. El parámetro
total
especifica cuántos clientes queremos que nos sean devueltos como respuesta.
Lo que estamos haciendo, en realidad, es incrementar la cantidad de conocimiento que el cliente debe tener predefinido para interactuar con el servicio (es decir, no sólo necesita saber la URI, sino además conocer la existencia de estos parámetros). Supongamos que en el futuro, el servidor decide que necesita cambiar la forma en la que se accede al número de datos solicitados por el cliente. Si el servidor cambia la interfaz, los clientes "antiguos" dejarán de funcionar a menos que cambien su código.
En lugar de publicar la interfaz REST anterior para obtener datos de los clientes, podemos incluir dicha información en el documento de respuesta, por ejemplo:
<clientes>
<link rel="next"
href="http://ejemplo.com/clientes?inicio=2&total=2"
type="application/xml"/>
<cliente id="123">
<nombre>Juan Garcia</nombre>
</cliente>
<cliente id="332">
<nombre>Pablo Bozo</nombre>
</cliente>
</clientes>
Cuando incluimos un enlace Atom en un documento, estamos asignando un nombre lógico a una transición de estados. En el ejemplo anterior, la transición de estados es el siguiente conjunto de clientes a los que podemos acceder. En lugar de tener que recordar cuáles son los parámetros de la URI que tenemos que utilizar en la siguiente invocación para obtener más clientes, lo único que tenemos que hacer es "seguir" el enlace proporcionado. El cliente no tiene que "contabilizar" en ningún sitio la interacción, ni tiene que recordar qué "sección" de la base de datos estamos consultando actualmente.
Además, el XML devuelto es auto-contenido. ¿Qué pasa si tenemos que "pasar" este documento a un tercero? Tendríamos que "decirle" que se trata de una vista parcial de la base de datos y especificar el ídice de inicio. Al incluir el enlace en el documento, ya no es necesario proporcionar dicha información adicional, ya que forma parte del propio documento
Reducción de errores de transición de estados
Los enlaces no se utilizan solamente como un mecanismo para agregar información de
"navegación". También se utilizan para cambiar el estado de los recursos. Pensemos
en una aplicación de comercio web a la que podemos acceder con la URI pedidos/333
:
<pedido id="333">
<cliente id="123">...</cliente>
<importe>99.99</importe>
<lineas-pedido>
...
</lineas-pedido>
</pedido>
Supongamos que un cliente quiere cancelar su pedido. Podría simplemente invocar
la petición HTTP DELETE /pedidos/333
. Esta no es siempre la mejor opción, ya que
normalmente el sistema necesitará "retener" el pedido para propósitos de almacenaje.
Por ello, podríamos considerar una nueva representación del pedido con un elemento
cancelado
a true:
PUT /pedidos/333 HTTP/1.1
Content-Type: application/xml
<pedido id="333">
<cliente id="123">...</cliente>
<importe>99.99</importe>
<cancelado>true</cancelado>
<lineas-pedido>
...
</lineas-pedido>
</pedido>
Pero, ¿qué ocurre si el pedido no puede cancelarse? Podemos tener un cierto estado en nuestro proceso de pedidos en donde esta acción no está permitida. Por ejemplo, si el pedido ya ha sido enviado, entonces no puede cancelarse. En este caso, realmente no hay nigún código de estado HTTP de respuesta que represente esta situación. Una mejor aproximación es incluir un enlace para poder realizar la cancelación:
<pedido id="333">
<cliente id="123">...</cliente>
<importe>99.99</importe>
<cancelado>false</cancelado>
<link rel="cancelar"
href="http://ejemplo.com/pedidos/333/cancelado"/>
<lineas-pedido>
...
</lineas-pedido>
</pedido>
El cliente podría invocar la orden: GET /pedidos/333
y obtener el documento XML
que representa el pedido. Si el documento contiene el enlace cancelar, entonces
el cliente puede cambiar el estado del pedido a "cancelado" enviando una orden PUT vacía
a la URI referenciada en el enlace. Si el documento no contiene el enlace, el cliente
sabe que esta operación no es posible. Esto permite que el servicio web controle en
tiempo real la forma en la que el cliente interactua con el sistema.
4.2.3. Enlaces en cabeceras frente a enlaces Atom
Una alternativa al uso de enlaces Atom en el cuerpo de la respuesta, es utilizar enlaces en las cabeceras de la respuesta (http://tools.ietf.org/html/rfc5988). Vamos a explicar ésto con un ejemplo.
Consideremos el ejemplo de cancelación de un pedido que acabamos de ver.
En lugar de utilizar un enlace Atom para especificar si se permite o no la cancelación
del pedido, podemos utilizar la cabecera Link
(es uno de los posibles campos que podemos
incluir como cabecera en una respuesta HTTP)).
De esta forma, si un usuario
envía la petición GET /pedidos/333
, recibirá la siguiente respuesta HTTP:
HTTP/1.1 200 OK
Content-Type: application/xml
Link: <http://ejemplo.com/pedidos/333/cancelado>; rel=cancel
<pedido id="333">
...
</pedido>
La cabecera Link
tiene las mismas características que un enlace Atom. La URI
está entre los signos <
y >
y está seguida por uno o más atributos delimitados
por ;
. El atributo rel
es obligatorio y tiene el mismo significado que el
correspondiente atributo Atom com el mismo nombre. En el ejemplo no se muestra,
pero podríamos especificar el media type utilizando el atributo type
.
4.3. HATEOAS y JAX-RS
JAX-RS no proporciona mucho soporte para implementar HATEOAS. HATEOAS se define por la aplicación, por lo que no hay mucho que pueda aportar ningún framework. Lo que sí proporciona JAX-RS son algunas clases que podemos utilizar para construir las URIs de los enlaces HATEOAS.
4.3.1. Construcción de URIs con UriBuilder
Una clase que podemos utilizar es javax.ws.rs.core.UriBuilder
. Esta clase nos permite
construir URIs elemento a elemento, y también permite incluir plantillas de parámetros
(segmentos de ruta variables).
public abstract class UriBuilder {
public static UriBuilder fromUri(URI uri)
throws IllegalArgumentException
public static UriBuilder fromUri(String uri)
throws IllegalArgumentException
public static UriBuilder fromPath(String path)
throws IllegalArgumentException
public static UriBuilder fromResource(Class<?> resource)
throws IllegalArgumentException
public static UriBuilder fromLink(Link link)
throws IllegalArgumentException
Las instancias de UriBuilder
se obtienen a partir de métodos estáticos con la forma
fromXXX()
. Podemos inicializarlas a partir de una URI, una cadena de caracteres, o
la anotación @Path de una clase de recurso.
Para extraer, modificar y/o componer una URI, se pueden utilizar métodos como:
public abstract UriBuilder clone(); // crea una copia
// crea una copia con la información de un objeto URI
public abstract UriBuilder uri(URI uri)
throws IllegalArgumentException;
// métodos para asignar/modificar valores de
// los atributos de los objetos UriBuilder
public abstract UriBuilder scheme(String scheme)
throws IllegalArgumentException;
public abstract UriBuilder userInfo(String ui);
public abstract UriBuilder host(String host)
throws IllegalArgumentException;
public abstract UriBuilder port(int port)
throws IllegalArgumentException;
public abstract UriBuilder replacePath(String path);
// métodos que añaden elementos a nuestra URI
public abstract UriBuilder path(String path)
public abstract UriBuilder segment(String... segments)
public abstract UriBuilder matrixParam(String name,
Object... values)
public abstract UriBuilder queryParam(String name,
Object... values)
// método que instancia el valor de una plantilla de la URI
public abstract UriBuilder resolveTemplate(String name,
Object value)
...
Los métodos build()
construyen la URI. Ésta puede contener plantillas de parámetros (
segmentos de ruta variables), que deberemos inicializar utilizando pares nombre/valor,
o bien una lista de valores que reemplazarán a los parámetros de la plantilla en el orden
en el que aparezcan.
public abstract URI buildFromMap(Map<String, ? extends Object> values)
throws IllegalArgumentException, UriBuilderException;
public abstract URI build(Object... values)
throws IllegalArgumentException, UriBuilderException;
...
}
Veamos algún ejemplo que muestra cómo crear, inicializar, componer y construir una URI
utilizando un UriBuilder
:
UriBuilder builder = UriBuilder.fromPath("/clientes/{id}");
builder.scheme("http")
.host("{hostname}")
.queryParam("param={param}");
Con este código, estamos definiendo una URI como:
http://{hostname}/clientes/{id}?param={param}
Puesto que tenemos plantillas de parámetros, necesitamos inicializarlos con
valores que pasaremos como argumentos para crear la URI final. Si queremos
reutilizar la URI que contiene las plantillas, deberíamos realizar una llamada a
clone()
antes de llamar al método build()
, ya que éste reemplazará los parámetros
de las plantillas en la estructura interna del objeto:
UriBuilder clone = builder.clone();
URI uri = clone.build("ejemplo.com", "333", "valor");
El código anterior daría lugar a la siguiente URI:
http://ejemplo.com/clientes/333?param=valor
También podemos definir un objeto de tipo Map
que contenga los valores de las
plantillas:
Map<String, Object> map = new HashMap<String, Object>();
map.put("hostname", "ejemplo.com");
map.put("id", 333);
map.put("param", "valor");
UriBuilder clone = builder.clone();
URI uri = clone.buildFromMap(map);
Otro ejemplo interesante es el de crear una URI a partir de las expresiones @Path definidas en las clases JAX-RS anotadas. A continuación mostramos el código:
@Path("/clientes")
public class ServicioClientes {
@Path("{id}")
public Cliente getCliente(@PathParam("id") int id) {...}
}
Podemos referenciar esta clase y el método getCliente()
a través de la clase
UriBuilder
de la siguiente forma:
UriBuilder builder = UriBuilder.fromResource(ServicioClientes.class);
builder.host("{hostname}")
builder.path(ServicioClientes.class, "getCliente");
El código anterior define la siguiente plantilla para la URI:
http://{hostname}/clientes/{id}
A partir de esta plantilla, podremos construir la URI utilizando alguno de los métodos `buildXXX().
También podemos querer utilizar UriBuilder para crear URIS a partir de plantillas. Para ello disponemos
de métodos resolveTemplateXXX()
, que nos facilitan el trabajo:
public abstract UriBuilder resolveTemplate(String name, Object value);
public abstract UriBuilder resolveTemplate(String name, Object value,
boolean encodeSlashInPath);
public abstract UriBuilder resolveTemplateFromEncoded(String name,Object value);
public abstract UriBuilder resolveTemplates(Map<String, Object> templateValues);
public abstract UriBuilder resolveTemplates(
Map<String,Object> templateValues, boolean encodeSlashInPath)
throws IllegalArgumentException;
public abstract UriBuilder resolveTemplatesFromEncoded(
Map<String, Object> templateValues);
// Devuelve la URI de la plantilla como una cadena de caracteres
public abstract String toTemplate()
Funcionan de forma similar a los métodos build()
y se utilizan para resolver
parcialmente las plantillas contenidas en la URI. Cada uno de los métodos devuelve
una nueva instancia de UriBuilder, de forma que podemos "encadenar" varias llamadas
para resolver todas las plantillas de la URI. Finalmente, usaremos el método toTemplate()
para obtener la nueva plantilla en forma de String
:
String original = "http://{host}/{id}";
String nuevaPlantilla = UriBuilder.fromUri(original)
.resolveTemplate("host", "localhost")
.toTemplate();
El valor de nuevaPlantilla para el código anterior sería: "http://localhost/{id}"
4.3.2. URIs relativas mediante el uso de UriInfo
Cuando estamos escribiendo servicios que "distribuyen" enlaces, hay cierta información que probablemente no conozcamos cuando estamos escribiendo el código. Por ejemplo, podemos no conocer todavía los hostnames de los enlaces, o incluso los base paths de las URIs, en el caso de que estemos enlazando con otros servicios REST.
JAX-RS proporciona una forma sencilla de solucionar estos problemas utilizando la
interfaz javax.ws.rs.core.UriInfo
. Ya hemos introducido algunas características de
esta interfaz en sesiones anteriores. Además de poder consultar información básica de la
ruta, también podemos obtener instancias de UriBuilder preinicializadas con la URI
base utilizada para definir los servicios JAX-RS, o la URI utilizada para invocar
la petición HTTP actual:
public interface UriInfo {
public URI getRequestUri();
public UriBuilder getRequestUriBuilder();
public URI getAbsolutePath();
public UriBuilder getAbsolutePathBuilder();
public URI getBaseUri();
public UriBuilder getBaseUriBuilder();
Por ejemplo, supongamos que tenemos un servicio que permite acceder a Clientes desde una
base de datos. En lugar de tener una URI base que devuelva todos los clientes en un
único documento, podemos incluir los enlaces previo
y sigiente
, de forma que podamos
"navegar" por los datos. Vamos a mostrar cómo crear estos enlaces utilizando la URI para
invocar la petición:
@Path("/clientes")
public class ServicioClientes {
@GET
@Produces("application/xml")
public String getCustomers(@Context UriInfo uriInfo) { (1)
UriBuilder nextLinkBuilder = uriInfo.getAbsolutePathBuilder(); (2)
nextLinkBuilder.queryParam("inicio", 5);
nextLinkBuilder.queryParam("total", 10);
URI next = nextLinkBuilder.build();
//... rellenar el resto del documento ...
}
...
}
1 | Para acceder a la instancia UriInfo que representa al petición, usamos la anotación
javax.ws.rs.core.Context , para inyectarla como un parámetro del método del recurso REST |
2 | Obtenemos un UriBuilder preininicializado con la URI utilizada para acceder al
servicio |
Para el código anterior, y dependiendo de cómo se despliegue el servicio, la URI creada podría ser:
http://org.expertojava/jaxrs/clientes?inicio=5&total=10
4.3.3. Construcción de enlaces (Links) en documentos XML y en cabeceras HTTP
JAX-RS proporciona cierto soporte para construir los enlaces y devolverlos en las
cabeceras de respuesta, o bien incluirlos
en los documentos XML. Para ello podemos utilizar las clases java.ws.rs.core.Link
y java.ws.rs.core.Link.Builder
.
public abstract class Link {
public abstract URI getUri();
public abstract UriBuilder getUriBuilder();
public abstract String getRel();
public abstract List<String> getRels();
public abstract String getTitle();
public abstract String getType();
public abstract Map<String, String> getParams();
public abstract String toString();
}
Link
es una clase abstracta que representa todos los metadatos contenidos en una
cabecera o en un enlace Atom. El método getUri()
representa el atributo href
del enlace
Atom. El método getRel()
representa el atributo rel
, y así sucesivamente. Podemos
referenciar a todos los atributos a través del método getParams()
. Finalmente,
el método toString()
convertirá la instancia Link
en una cadena de caracteres con
el formato de una cabecera Link
.
Para crear instancias de Link
utilizaremos un Link.Builder
, que crearemos con
alguno de estos métodos:
public abstract class Link {
public static Builder fromUri(URI uri)
public static Builder fromUri(String uri)
public static Builder fromUriBuilder(UriBuilder uriBuilder)
public static Builder fromLink(Link link)
public static Builder fromPath(String path)
public static Builder fromResource(Class<?> resource)
public static Builder fromMethod(Class<?> resource, String method)
...
}
Los métodos fromXXX()
funcionan de forma similar a UriBuilder.fromXXX()
.
Todos inicializan el UriBuilder
subyacente que utilizaremos para construir el atributo
href
del enlace.
Los métodos link()
, uri()
, y uriBuilder()
nos permiten sobreescribir la
URI subyacente del enlace que estamos creando:
public abstract class Link {
interface Builder {
public Builder link(Link link);
public Builder link(String link);
public Builder uri(URI uri);
public Builder uri(String uri);
public Builder uriBuilder(UriBuilder uriBuilder);
...
Los siguientes métodos nos permiten asignar valores a varios atributos del enlace que estamos construyendo:
...
public Builder rel(String rel);
public Builder title(String title);
public Builder type(String type);
public Builder param(String name, String value);
...
Finalmente, él método build()
nos permitirá construir el enlace:
public Link build(Object... values);
El objeto Link.Builder
tiene asociado una UriBuilder
subyacente. Los valores
pasados como parámetros del método build()
son utilizados por el UriBuilder
para
crear una URI para el enlace. Veamos un ejemplo:
Link link = Link.fromUri("http://{host}/raiz/clientes/{id}")
.rel("update").type("text/plain")
.build("localhost", "1234");
Si realizamos una llamada a toString()
sobre la instancia del enlace (link
),
obtendremos lo siguiente:
http://localhost/raiz/clientes/1234>; rel="update"; type="text/plain"
A continuación mostramos dos ejemplos que muestran cómo crear instancias Link
en
las cabeceras, y en el cuerpo de la respuesta como un enlace Atom:
@Path
@GET
Response get() {
Link link = Link.fromUri("a/b/c").build();
Response response = Response.noContent()
.links(link)
.build();
return response; }
import javax.ws.rs.core.Link;
@XmlRootElement
public class Cliente {
private String nombre;
private List<Link> enlaces = new ArrayList<Link>();
@XmlElement
public String getNombre() {
return nombre;
}
public void setNombre(String nom) {
this.nombre = nom;
}
@XmlElement(name = "enlace")
@XmlJavaTypeAdapter(Link.JaxbAdapter.class) (1)
public List<Link> getEnlaces() {
return enlaces; }
}
1 | La clase Link contiene también un JaxbAdapter , con una implementación de
la clase JAXB XmlAdapter , que "mapea" los objetos JAX-RS de tipo Link a un
valor que puede ser serializado y deserializado por JAXB |
El código de este ejemplo permite construir cualquier enlace y añadirlo a la clase
Cliente
de nuestro dominio. Los enlaces serán convertidos a elementos XML, que se
incluirán en el documento XML de respuesta.
4.4. Seguridad
Es importante que los servicios rest permitan un acceso seguro a los datos y funcionalidades que proporcionan. Especialmente para servicios que permiten la realización de actualizaciones en los datos. También es interesante asegurarnos de que terceros no lean nuestros mensajes, e incluso permitir que ciertos usuarios accedan a determinadas funcionalidades pero a otras no.
Además de la especificación JAX-RS, podemos aprovechar los servicios de seguridad que nos ofrece la web y Java EE, y utilizarla en nuestros servicios REST. Estos incluyen:
- Autentificación
-
Hace referencia a la validación de la identidad del cliente que accede a los servicios. Normalmente implica la comprobación de si el cliente ha proporcionado unos credenciales válidos, tales como el password. En este sentido, podemos utilizar los mecanismos que nos proporciona la web, y las facilidades del contenedor de servlets de Java EE, para configurar los protocolos de autentificación.
- Autorización
-
Una vea que el cliente se ha autenticado (ha validado su identidad), querrá interactuar con nuestro servicio REST. La autorización hace referencia a decidir si un cierto usuario puede acceder e invocar un determinado método sobre una determinada URI. Por ejemplo, podemos habilitar el acceso a operaciones PUT/POST/DELETE para ciertos usuarios, pero para otros no. En este caso, utilizaremos las facilidades que nos propociona el contenedor de servlets de Java EE, para realizar autorizaciones.
- Encriptado
-
Cuando un cliente está interaccionando con un servicio REST, es posible que alguien intercepte los mensajes y los "lea", si la conexión HTTP no es segura. Los datos "sensibles" deberían protegerse con servicios criptográficos, tales como SSL.
4.4.1. Autentificación en JAX-RS
Hay varios protocolos de autentificación. En este caso, vamos a ver cómo realizar una autenticación básica sobre HTTP (y que ya habéis utilizado para servlets). Este tipo de autentificación requiere enviar un nombre de usuario y password, codificados como Base-64, en una cabecera de la petición al servidor. El servidor comprueba si existe dicho usuario en el sistema y verifica el password enviado. Veámoslo con un ejemplo:
Supongamos que un cliente no autorizado quiere acceder a nuestros servicios REST:
GET /clientes/333 HTTP/1.1
Ya que la petición no contiene información de autentificación, el servidor debería responder la siguiente respuesta:
HTTP/1.1 401 Unauthorized
WWW-Autenticate: Basic realm="Cliente Realm"
La respuesta 401
nos indica que el cliente no está autorizado a acceder a dicha URI.
La cabecera WWW-Autenticate
especifica qué protocolo de autentificación se debería usar.
En este caso, Basic
significa que se debería utilizar una autentificación de tipo
Basic
. El atributo realm
identifica una colección de recursos seguros en un sitio web.
En este ejemplo, indica que solamente están autorizados a acceder al método GET a través
de la URI anterior, todos aquellos uarios que pertenezcan al realm Cliente Realm
, y
serán autentificados por el servidor mediante una autentificación básica.
Para poder realizar la autentificación, el cliente debe enviar una petición que
incluya la cabecera Authorization
, cuyo valor sea Basic
, seguido de la siguiente cadena de
caracteres login:password codificada en Base64 (el valor de login y password
representa el login y password del usuario). Por ejemplo, supongamos que el nombre del
usuario es felipe
y el password es locking
, la cadena felipe:locking
codificada
como Base64 es ZmVsaXBlOmxvY2tpbmc=
. Por lo tanto, nuestra petición debería ser
la siguiente:
GET /clientes/333 HTTP/1.1
Authorization: Basic ZmVsaXBlOmxvY2tpbmc=
El cliente debería enviar esta cabecera con todas y cada una de las peticiones que haga al servidor.
El inconveniente de esta aproximación es que si la petición es interceptada por alguna entidad "hostil" en la red, el hacker puede obtner fácilmente el usuario y el passwork y utilizarlos para hacer sus propias peticiones. Utilizando una conexión HTTP encriptada (HTTPS), se soluciona este problema.
Creación de usuarios y roles
Para poder utilizar la autentificación básica necesitamos tener creados previamente los
realms en el servidor de aplicaciones Wildfly, y registrar los usuarios que pertenecen a dichos
realms. La forma de hacerlo es idéntica a lo que ya habéis visto en la asignatura
de Componentes Web (a través del comando add-user.sh
).
Utilizaremos el realm por defecto "ApplicationRealm" de Wildfly, que nos permitirá además, controlar la autorización mediante la asignación de roles a usuarios.
Lo único que tendremos que hacer es añadir los usuarios a dicho realm, a través de
la herramienta $WILDFLY_HOME/bin/add-user.sh
Al ejecutarla desde línea de comandos, deberemos elegir el ream "ApplicationRealm" e introducir los datos para cada nuevo usuario que queramos añadir, indicando su login, password, y el grupo (rol) al que queremos que pertenezca dicho usuario.
Los datos sobre los nuevos usuarios creados se almacenan en los ficheros:
application-users.properties
y application-roles.properties
, tanto en el
directorio $WILDFLY_HOME/standalone/configuration/
, como en
$WILDFLY_HOME/domain/configuration/
Una vez creados los usuarios, tendremos que incluir en el fichero de
configuración web.xml
, la siguiente información:
<web-app>
...
<login-config> (1)
<auth-method>BASIC</auth-method>
<realm-name>ApplicationRealm</realm-name> (2)
</login-config>
<security-constraint>
<web-resource-collection>
<web-resource-name>customer creation</web-resource-name>
<url-pattern>/rest/resources</url-pattern> (3)
<http-method>POST</http-method> (4)
</web-resource-collection>
...
</security-constraint>
...
</web-app>
1 | El elemento <login-config> define cómo queremos autentificar
nuestro despliegue. El subelemento <auth-method> puede tomar los valores
BASIC , DIGEST , or CLIENT_CERT , correspondiéndose con la autentificación Basic,
Digest, y Client Certificate, respectivamente. |
2 | El valor de la etiqueta <realm-name> es el que se mostrará como valor del atributo
realm de la cabecera WWW-Autenticate , si intentamos acceder al recurso sin
incluir nuestras credenciales en la petición. |
3 | El elemento <login-config> realmente NO "activa" la autentificación. Por defecto,
cualquier cliente puede acceder a cualquier URL proporcionada por nuestra aplicación
web sin restricciones. Para forzar la autentificación, debemos especificar el
patrón URL que queremos asegurar (elemento <url-pattern> ) |
4 | El elemento <http-method> nos indica que solamente queremos asegurar las peticiones
POST sobre esta URL. Si no incluimos el elemento <http-method> , todos los métodos
HTTP serán seguros. En este ejemplo, solamente queremos asegurar los métodos POST
dirigidos a la URL /rest/resources |
4.4.2. Autorización en JAX-RS
Mientras que la autentificación hacer referencia a establecer y verificar la identidad del usuario, la autorización tiene que ver con los permisos. ¿El usuario X está autorizado para acceder a un determinado recurso REST?
JAX-RS se basa en las especificaciones Java EE y de servlets para definir
la forma de autorizar a los usuarios. En Java EE, la autorización se realiza asociando
uno o más roles con un usuario dado y, a continuación asignando permisos basados
en dicho rol. Ejemplos de roles pueden ser: administrador
, empleado
. Cada rol
tiene asignando unos permisos de acceso a determinados recursos, por lo que asignaremos
los permisos utilizando cada uno de los roles.
Para poder realizar la autorización, tendremos que incluir determinadas etiquetas
en el fichero de configuración web.xml
(tal y como ya habéis visto en la asignatura de
Componentes Web). Veámoslo con un ejemplo (en el que también incluiremos autentificación):
Volvamos a nuestra aplicación de venta de productos por internet. En esta aplicación, es
posible crear nuevos clientes enviando la información en formato XML a un recurso JAX-RS
localizado por la anotación @Path("/clientes")
. El servicio REST es desplegado y
escaneado por la clase Application
anotada con @ApplicationPath("/servicios")
,
de forma que la URI completa es /servicios/clientes
. Queremos proporcionar seguridad
a nuestro servicio de clientes de forma que solamente los administradores puedan crear
nuevos clientes. Veamos cuál sería el contenido del fichero web.xml
:
<?xml version="1.0"?>
<web-app>
<security-constraint>
<web-resource-collection>
<web-resource-name>creacion de clientes</web-resource-name>
<url-pattern>/servicios/clientes/*</url-pattern>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint> (1)
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>ApplicationRealm</realm-name>
</login-config>
<security-role> (2)
<role-name>admin</role-name>
</security-role>
</web-app>
1 | Especificamos qué roles tienen permiso para acceder mediante POST a la URL
/services/customers . Para ello utilizamos el elemento <auth-constraint> dentro de
<security-constraint> . Este elemento tiene uno o más subelementos <role-name> ,
que definen qué roles tienen permisos de acceso definidos por <security-constraint> .
En nuestro ejemplo, estamos dando al rol admin permisos para acceder a la
URL /services/customers/ con el método POST. Si en su lugar indicamos un <role-name> con
el valor * , cualquier usuario podría acceder a dicha URL. En otras palabras, un <role-name>
con el valor * significa que cualquier usuario que sea capaz de autentificarse, puede
acceder al recurso. |
2 | Para cada <role-name> que usemos en nuestras declaraciones <auth-constraints> , debemos
definir el correspondiente <security-role> en el descriptor de despliegue. |
Una limitación cuando estamos declarando las <security-contraints>
para los recursos
JAX-RS es que el elemento <url-pattern>
solamente soporta el uso de *
en el patrón url
especificado. Por ejemplo: /*
, /rest/*
, \*.txt
.
4.4.3. Encriptación
Por defecto, la especificación de servlets no requiere un acceso a través de HTTPS.
Si queremos forzar un acceso HTTPS, podemos especificar un elemento <user-data-constraint>
como parte de nuestra definición de restricciones de seguridad (<security-constraint>
).
Vamos a modificar nuestro ejemplo anterior para forzar un acceso a través de HTTPS:
<web-app>
...
<security-constraint>
<web-resource-collection>
<web-resource-name>creacion de clientes</web-resource-name>
<url-pattern>/servicios/clientes/*</url-pattern>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee> (1)
</user-data-constraint>
</security-constraint>
...
</web-app>
1 | Todo lo que tenemos que hacer es declarar un elemento <transport-guarantee>
dentro de <user-data-constraint> con el valor CONFIDENTIAL . Si un usuario intenta
acceder a una URL con el patrón especificado a través de HTTP, será redirigido a
una URL basada en HTTPS. |
Anotaciones JAX-RS para autorización
Java EE define un conjunto de anotaciones para definir metadatos de autorización. La
especificación JAX-RS sugiere, aunque no es obligatorio, que las implementaciones
por diferentes vendedores den soporte a dichas anotaciones. Éstas se encuentran en
el paquete javax.annotation.security
y son: @RolesAllowed, @DenyAll, @PermitAll, y @RunAs.
La anotación @RolesAllowed
define los roles permitidos para ejecutar una determinada
operación. Si anotamos una clase JAX-RS, define el acceso para todas las operaciones
HTTP definidas en la clase JAX-RS. Si anotamos un método JAX-RS, la restricción se
aplica solamente al método que se está anotando.
La anotación @PermitAll especifica que cualquier usuario autentificado puede invocar a nuestras operaciones. Al igual que @RolesAllowed, esta anotación puede usarse en la clase, para definir el comportamiento por defecto de toda la clase, o podemos usarla en cada uno de los métodos. Veamos un ejemplo:
@Path("/clientes")
@RolesAllowed({"ADMIN", "CLIENTE"}) (1)
public class ClienteResource {
@GET
@Path("{id}")
@Produces("application/xml")
public Cliente getClienter(@PathParam("id") int id) {...}
@RolesAllowed("ADMIN") (2)
@POST
@Consumes("application/xml")
public void crearCliente(Customer cust) {...}
@PermitAll (3)
@GET
@Produces("application/xml")
public Customer[] getClientes() {}
}
1 | Por defecto, solamente los usuarios con rol ADMIN y CLIENTE pueden ejecutar los métodos HTTP definidos en la clase ClienteResource |
2 | Sobreescribimos el comportamiento por defecto. Para el método crearCliente()
solamente permitimos peticiones de usuarios con rol ADMIN |
3 | Sobreescribimos el comportamiento por defecto. Para el método getClientes()
de forma que cualquier usuario autentificado puede acceder a esta operación a través
de la URI correspondiente, con el método GET. |
La ventaja de utilizar anotaciones es que nos permite una mayor flexibilidad que
el uso del fichero de configuración web.xml
, pudiendo definir diferentes autorizaciones
a nivel de método.
4.4.4. Seguridad programada
Hemos visto como utilizar una seguridad declarativa, es decir, basándonos en meta-datos definidos estáticamente antes de que la aplicación se ejecute. JAX-RS proporciona una forma de obtener información de seguridad que nos permite implementar seguridad de forma programada en nuestras aplicaciones.
Podemos utilizar la interfaz javax.ws.rs.core.SecurityContext
para determinar la
identidad del usuario que realiza la invocación al método proporcionando sus credenciales.
También podemos comprobar si el usuario pertenece o no a un determinado rol:
Esto nos permite implementar seguridad de forma programada en nuestras aplicaciones.
public interface SecurityContext {
public Principal getUserPrincipal();
public boolean isUserInRole(String role);
public boolean isSecure();
public String getAuthenticationScheme();
}
El método getUserPrincipal()
devuelve un objeto de tipo javax.security.Principal
,
que representa al usuario que actualmente está realizando la petición HTTP
El método isUserInRole()
nos permite determinar si el usuario que realiza
la llamada actual pertenece a un determinado rol.
El método isSecure()
devuelve cierto si la petición actual es una conexión segura.
El método getAuthenticationScheme()
nos indica qué mecanismo de autentificación se
ha utilizado para asegurar la petición (valores típicos devueltos por el método
son: BASIC
, DIGEST
, CLIENT_CERT
, y FORM
).
Podemos acceder a una instancia de SecurityContext inyectándola en un campo,
método setter, o un parámetro de un recurso, utilizando la anotación @Context
.
Veamos un ejemplo. Supongamos que queremos obtener un fichero de log con todos
los accesos a nuestra base de datos de clientes hechas por usuarios que no son
administradores:
@Path("/clientes")
public class CustomerService {
@GET
@Produces("application/xml")
public Cliente[] getClientes(@Context SecurityContext sec) {
if (sec.isSecure() && !sec.isUserInRole("ADMIN")) {
logger.log(sec.getUserPrincipal()
+ " ha accedido a la base de datos de clientes");
}
...
}
}
En este ejemplo, inyectamos una instancia de SecurityContext
como un parámetro del
método getClientes()
. Utilizamos el método SecurityContext.isSecure()
para determinar
si se trata de una petición realizada a través de un canal seguro (como HTTPS). A continuación utilizamos el método
SecurityContext.isUserInRole()
para determinar si el usuario que realiza la llamada
tiene el rol ADMIN o no. Finalmente, imprimimos el resultado en nuestro fichero de logs.
Con la introducción del API de filtros en JAX-RS 2.0, podemos implementar la
interfaz SecurityContext
y sobreescribir la petición actual sobre SecurityContext
,
utilizando el método ContainerRequestContext.setSecurityContext()
. Lo interesante de
esto es que podemos implementar nuestros propios protocolos de seguridad. Por ejemplo:
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.HttpHeaders;
@PreMatching
public class CustomAuth implements ContainerRequestFilter {
protected MyCustomerProtocolHandler customProtocol = ...;
public void filter(ContainerRequestContext requestContext)
throws IOException {
String authHeader = request.getHeaderString(HttpHeaders.AUTHORIZATION);
SecurityContext newSecurityContext = customProtocol.validate(authHeader);
requestContext.setSecurityContext(authHeader);
}
}
Este filtro no muestra todos los detalles, pero sí la idea. Extrae la cabecera
Authorization
de la petición y la pasa a nuestro propio servicio customerProtocol
.
Éste devuelve una implementación de SecurityContext
. Finalmente sobreescribimos
el SecurityContext por defecto utilizando la nueva implementación.
No vamos a explicar el API de filtros de JAS-RS 2.0. Como ya habéis visto en la asignatura de Componentes Web, los filtros son objetos que se "interponen" entre el procesamiento de las peticiones, tanto del servidor como del cliente.
El filtro mostrado en el ejemplo es un filtro de petición en la parte del servidor. Este tipo de filtros se ejecuta antes de que se invoque a un método JAX-RS.
4.5. Ejercicios
Para los ejercicios de esta sesión proporcionamos el MÓDULO s4-foroAvanzado, que tendréis que usar como plantilla para realizar las tareas planteadas.
El proyecto está estructurado lógicamente en los siguientes paquetes:
-
org.expertojava.negocio
-
org.expertojava.rest
A su vez, cada uno de ellos contiene los subpaquetes api
y modelo
, con las clases
relacionadas con los servicios proporcionados, y los datos utilizados por los servicios, respectivamente.
El API rest implementado es el siguiente:
-
Recurso UsuariosResource.java
-
GET /usuarios, proporciona un listado con los usuarios del foro
-
-
Subrecurso UsuarioResource.java
-
GET /usuarios/login, proporciona información sobre el usuario cuyo login es "login"
-
PUT /usuarios/login, actualiza los datos de un usuario
-
DELETE /usuarios/login, borra los datos de un usuario
-
GET /usuarios/login/mensajes, obtiene un listado de los mensajes de un usuario
-
-
Recurso MensajesResource.java
-
GET /mensajes, proporciona un listado con los mensajes del foro
-
POST /mensajes, añade un mensaje nuevo en el foro
-
GET /mensajes/id, proporciona información sobre el mensaje cuyo id es "id"
-
PUT /mensajes/id, modifica un mensaje
-
DELETE /mensajes/id, borra un mensaje
-
Una vez desplegada la aplicación, podéis añadir datos a la base de datos del foro, utilizando los datos del fichero /src/main/resources/foro.sql. Para ello simplemente tendréis que invocar la goal Maven correspondiente desde la ventana Maven Projects > s4-foroAvanzado > Plugins > sql > sql:execute
En el directorio src/main/resources
tenéis un fichero de texto (instrucciones.txt
)
con información adicional sobre la implementación proporcionada.
A partir de las plantillas, se pide:
4.5.1. Uso de Hateoas (1 puntos)
Vamos a añadir a los servicios enlaces a las operaciones que podemos realizar con cada recurso, siguiendo el estilo Hateoas.
-
Para los usuarios:
-
En el listado de usuarios añadir a cada usuario un enlace con relación
self
que apunte a la dirección a la que está mapeado el usuario individual. -
En la operación de obtención de un usuario individual, incluir los enlaces para ver el propio usuario (self), modificarlo (usuario/modificar), borrarlo (usuario/borrar), o ver los mensajes que envió el usuario (usuario/mensajes).
-
-
Para los mensajes:
-
En el listado de mensajes añadir a cada mensaje un enlace con relación
self
que apunte a la dirección a la que está mapeado el mensaje individual. -
En la operación de obtención de un mensaje individual, incluir los enlaces para ver el propio mensaje (self), modificarlo (mensaje/modificar), borrarlo (mensaje/borrar), o ver los datos del usuario que envió el mensaje (mensaje/usuario).
-
Utiliza postman para comprobar las modificaciones realizadas.
4.5.2. Ejercicio seguridad (1 punto)
Vamos ahora a restringir el acceso al servicio para que sólo usuarios registrados puedan realizar modificaciones. Se pide:
-
Añadir al usuario "pepe" en el "ApplicationRealm" de wildfly, con la contraseña "pepe", y perteneciente al grupo (rol) "registrado"
-
Configurar, mediante seguridad declarativa, para que las operaciones de modificación (POST, PUT y DELETE) sólo la puedan realizar los usuarios con rol registrado. Utilizar autentificación de tipo BASIC.
-
Ahora vamos a hacer que la modificación o borrado de usuarios sólo pueda realizarlas el mismo usuario que va a modificarse o borrarse. Para ello utilizaremos seguridad programada. En el caso de que el usuario que va a realizar la modificación o borrado quiera borrar/modificar otros usuarios lanzaremos la excepción
WebApplicationException(Status.FORBIDDEN)
-
Vamos a hacer lo mismo con los mensajes. Sólo podrá modificar y borrar mensajes el mismo usuario que los creó, y al publicar un nuevo mensaje, forzaremos que el login del mensaje sea el del usuario que hay autentificado en el sistema.
Utiliza postman para comprobar las modificaciones realizadas.