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 atributo self

  • 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).

Clase UriBuilder: métodos para instanciar objetos de la clase
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:

Clase UriBuilder: métodos para "manipular" las URIs
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.

Clase UriBuilder: métodos buildXXX() para construir las URIs
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:

Clase UriBuilder: métodos resolveTemplateXXX() para crear URIs a partir de plantillas
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

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.

Clase abstracta javax.ws.rs.core.Link
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:

Escritura de enlaces en cabeceras HTTP
@Path
@GET
Response get() {
    Link link = Link.fromUri("a/b/c").build();
    Response response = Response.noContent()
                                .links(link)
                                .build();
    return response; }
Inclusión de un enlace Atom en el documento XMl de respuesta
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.

  1. 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).

  2. 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:

  1. Añadir al usuario "pepe" en el "ApplicationRealm" de wildfly, con la contraseña "pepe", y perteneciente al grupo (rol) "registrado"

  2. 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.

  3. 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)

  4. 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.