Skip to main content

Securizar servicios REST Java: JAX-RS con JWT

En esta entrada mostraremos como utilizar tokens para securizar nuestros servicios REST hechos con JAX-RS. Los token los crearemos con la librería JJWT, que implementa el estándar JWT, que es el habitual para la creación de tokens.

Podríamos haber securizado los servicios mediante sesiones, como cualquier otra aplicación Java, sin embargo, esto no es lo habitual ni lo recomendado para servicios REST sin estado. Podéis ver más información al respecto en: https://ponyfoo.com/articles/json-web-tokens-vs-session-cookies

Las herramientas que debemos tener instaladas en nuestro equipo para el ejemplo son:

  • JDK 8
  • Un servidor Java. Para nuestro ejemplo utilizaremos Tomcat 8

Debemos tener también nuestra aplicación web Java creada con algunos servicios REST. Si no es así, puedes ver como crearla de forma sencilla en esta página: https://www.infointernet.es/internet-avanzado/java/crear-servicios-rest-json-una-aplicacion-web-java/.

Qué es JWT

JSON Web Token (JWT) es un estándar (RFC-7519 ) para crear un token de acceso.

Estos tokens están formados por tres partes:

  • Cabecera (header): es una cadena JSON cifrada en Base64 que contiene el tipo de token y la codificación utilizada para la firma
     Header = '{"alg": "HS256", "typ": "JWT"}'
  • Cuerpo (payload): Es un conjunto de información en formato JSON que contiene los “claims”, que no son más que un conjunto de campos predefinidos, así como algunos propios que podemos añadir, como los roles.
    Payload = '{“sub”:”j.rodriguez”,"iat": "1424180385", "exp": "1424190385",”roles”:”a,b,c”}'
  • Firma (signature): Está formado por la cabecera y cuerpo, encriptado con una clave secreta. Esta parte es la que garantiza la integridad de los datos, es decir, que los datos anteriores no han sido alterados:
     clave = 'secretkey'
     unsignedToken = encodeBase64Url (encabezado) + '.' + EncodeBase64Url (carga útil)
     signature = HMAC-SHA256 (clave, unsignedToken)
    

Estas partes se unen por un “.”. De este modo, el Token resultante podría tener un aspecto como el siguiente:

token = eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MDA5OTY4MDIsImV4cCI6MTUwMTAwMDQwMn0.GI-e8ala__SygMKviTShtLK9gXMTOKQPn0ktUh31988XyyGMBo4MbWLa1t71ybNuYplKhSjAaasxKbHWiMNwTA

Esta información viajará normalmente en la cabecera “Authorization” en los accesos a recursos protegidos.

Authorization: Bearer token

Puedes encontrar más información en https://carlosazaustre.es/blog/que-es-la-autenticacion-con-token/

Paso 1: Añadir dependencias

Debemos añadir a nuestro proyecto la dependencia de JJWT, que será la que nos permitirá generar y validar el token en formato JWT.

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.7.0</version>
</dependency>

Paso 2: Crear el filtro

El filtro será el encargado de interceptar las peticiones a recursos REST protegidos y validar el token recibido en la cabecera. Para ellos debe implementar la interfaz ContainerRequestFilter, que forma parte del estándar JAX-RS.

@Provider
@Secured
@Priority(Priorities.AUTHENTICATION)
public class RestSecurityFilter implements ContainerRequestFilter {
	
    public static final Key KEY = MacProvider.generateKey();
 
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
 
        // Recupera la cabecera HTTP Authorization de la petición
        String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
 
        try {
        	// Extrae el token de la cabecera
            String token = authorizationHeader.substring("Bearer".length()).trim();
 
            // Valida el token utilizando la cadena secreta
            Jwts.parser().setSigningKey(KEY).parseClaimsJws(token);
 
        } catch (Exception e) {
            requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
        }
    }
}

Para generar la cadena secreta hemos utilizado la clase “MacProvider” de la propia libreria JWT. El método usado genera una clave secreta aleatoria de 512 bits. Podría utilizarse también alguna clave almacenada en un keystore.

La anotación “@Provider” será la que nos permitirá registrar automáticamente nuestro filtro en nuestra aplicación REST.

Paso 3: Anotación para vincular el filtro las clases y métodos protegidos

El filtro creado anteriormente no debe ejecutarse siempre, sino sólo en aquellos casos en los que el recurso esté protegido. Para ello crearemos una anotación que utilizará la meta-anotación “@NameBinding”, que forma parte de JAX-RS.

@javax.ws.rs.NameBinding
@Retention(RUNTIME)
@Target({ TYPE, METHOD })
public @interface Secured {

}

Paso 4: Securizar los recursos con la nueva anotación

Los métodos y clases que queramos securizar deberán ir precedidos por la anotación “@Secured” que hemos creado en el paso anterior.

@Path("/pelicula")
public class PeliculaResource {
	
    @GET
    @Path("{idPelicula}")
    @Secured
    @Produces({MediaType.APPLICATION_JSON+";charset=utf-8"}) 
    public Pelicula getPelicula(@PathParam("idPelicula") Integer idPelicula) throws Exception{
		
        //Habría que llamar a un servicio que devolviera la película con el id pasado como parámetro
        return getPeliculaEjemplo(); 
    }
    ...

Cuando se invoque al método del ejemplo anterior, se ejecutará previamente el filtro, comprobando que viaja la cabecera “Authorization” con un token válido. En caso de que haya cualquier problema, se devolvería un error HTTP 401. Si todos es correcto, se daría paso a la ejecución del método del ejemplo.

Paso 5: Crear un método para la obtención del token

Creamos un nuevo recurso con un método público que nos permitirá realizar la autenticación y obtener un token en caso positivo.

@Path("/usuario")
public class UsuarioResource {
 
    @GET
    @Path("/login")
    public Response authenticateUser(@QueryParam("login") String login,
    		@QueryParam("password") String password) {
        try {
 
            // Aquí iría el código de validación del usuario y contraseñas proporcionados,
        	// por ejemplo validándolo contra una base de datos...
            //authenticate(login, password);
        	
            // Si todo es correcto, generamos el token
            String token = issueToken(login);
 
            // Devolvemos el token en la cabecera "Authorization". 
            // Se podría devolver también en la respuesta directamente.
            return Response.ok().header(HttpHeaders.AUTHORIZATION, "Bearer " + token).build();
 
        } catch (Exception e) {
            return Response.status(Response.Status.UNAUTHORIZED).build();
        }
    }
 
    private String issueToken(String login) {
    	//Calculamos la fecha de expiración del token
    	Date issueDate = new Date();
    	Calendar calendar = Calendar.getInstance();
    	calendar.setTime(issueDate);
    	calendar.add(Calendar.MINUTE, 60);
        Date expireDate = calendar.getTime();
        
        //Creamos el token
        String jwtToken = Jwts.builder()
                .setSubject(login)
                .setIssuer("http://www.infointernet.es")
                .setIssuedAt(issueDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, RestSecurityFilter.KEY)
                .compact();
        return jwtToken;
    }
}

Hemos utilizado para la creación del token la misma clave secreta que en la validación, para que todo funcione correctamente.

Puedes descargarte el código del ejemplo en: https://github.com/mpecero/jaxrs-jwt-example

Espero que esta entrada te haya resultado útil. Por favor, valórala y deja tu comentario si tinenes cualquier duda.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Uso de cookies

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

CERRAR