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.
Estimado buenas tardes, tengo un proyecto y querido crear roles de acceso a mis servicios web. he seguido paso a paso el tutorial pero pienso que falta algo. Tengo imnplementado @RolesAllowed pero de igual forma se consume el servico con cualquier rol que ponga. El proyecto que tiene alojado para descargar es donde crea el token pero este tutorial no es el qeu se encuentra disponible para poder tomar como referencia. Quisiera si no le es molestia y pudiera guiarme en lo que me este faltando. Muchas gracias, tenga buen dia.
Hola Yoan
Acabo de subir a GitHub (https://github.com/mpecero/jaxrs-jwt-example) unos cambios que tenía por subir con respeto al ejemplo de los roles. Espero que te sirvan de ayuda.
Sin ver tu código es difícil ayudarte. ¿Te devuelve erro 401 cuando no va el token en la cabecera Authorization?
Descarga el código de GitHub y me cuentas.
Saludos