En esta entrada explicaremos como añadir una autorización basada en roles a un servicio REST Java hecho con JAX-RS y securizado con token JWT.
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/.
En la siguiente entrada podemos ver cómo proteger los servicios REST mediante el uso de token JWT, con la librería JJWT: http://www.infointernet.es/internet-avanzado/java/securizar-servicios-rest-java-jax-rs-jwt/
Una vez que tenemos nuestros servicios REST protegidos con usuario y contraseña, es el momento de añadir autorización por roles. De este modo podremos dar permisos a ciertos usuarios para realizar determinadas operaciones, mientras se los denegamos a otros usuarios.
Usar la anotación @RolesAllowed
La anotación @RolesAllowed pertenece al estándar de Java (JSR 250). Se puede utilizar tanto en clases como en métodos concretos. Sirve para definir los roles que pueden acceder a dichas clases o métodos. Siguiendo con nuestro ejemplo, si tenemos un rol “ADMINISTRADOR” que es el único que puede ejecutar un método para añadir películas, pondríamos la anotación en dicho método del siguiente modo:
@PUT
@Secured
@RolesAllowed({"ADMINISTRADOR"})
public void addPelicula() throws Exception{
// Habría que llamar a un servicio para crear películas
}
La anotación @Secured es la que nos creamos a medida en el ejemplo de securización para asociar el método al filtro de seguridad.
Añadir la información del usuario y sus roles al token de seguridad (JWT)
En el cuerpo (playload) de un token JWT puede viajar cualquier propiedad (claim) que nosotros creemos. En nuestro caso, crearemos una llamada “roles” en la que pondremos los roles del usuario separados por coma. El nombre de usuario viajará en una de las propiedades por defecto que define el estándar y que se denomina “subject”. Con esta información será suficiente para saber en cada petición si un usuario tiene permisos para ejecutar un método o no, según las anotaciones @RolesAllowed que le afecten.
En nuestro ejemplo, tendremos que modificar el método utilizado para crear el token (JJWT). Ahí añadiremos una propiedad inicial con los roles de usuario logado. Dichos roles provendrán de una base de datos, etc. De este modo nuestro método de creación del token quedaría así:
private String issueToken(String login, String roles) {
//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()
.claim("roles", roles)
.setSubject(login)
.setIssuer("http://www.infointernet.es")
.setIssuedAt(issueDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, RestSecurityFilter.KEY)
.compact();
return jwtToken;
}
Hemos introducido los roles en el propio token para no tener que estar haciendo peticiones a base de datos en cada petición para recuperar los roles del usuario que realiza dicha petición. Los datos que viajan en el token son seguros e inalterables, porque en caso de modificación, el token dejaría de ser válido. De ahí que podamos utilizarlo para almacenar información de autorización.
Crear un bean que represente al usuario y que implemente Principal
Crearemos un bean con las propiedades del usuario logado que deseemos tener accesibles en nuestros métodos REST. Dicha clase debe implementar la interfaz del estándar Java llamada “Principal“.
public class User implements Principal {
private String username;
private List roles;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getName() {
return username;
}
public List getRoles() {
return roles;
}
public void setRoles(List roles) {
this.roles = roles;
}
}
Crear una clase que implemente SecurityContext
La interfaz SecurityContext forma parte del estándar de Java y sirve para almacenar información relativa a la seguridad. Entre los métodos que tenemos que implementar está “isUserInRole” que devuelve un booleano indicando si el usuario logado tiene el rol pasado como parámetro al método. Este método es el que se invocará con la anotación @RolesAllowed y determinará los permisos en la aplicación.
Nuestra clase quedaría así:
public class MyApplicationSecurityContext implements SecurityContext {
private User usuario;
private boolean secure;
public MyApplicationSecurityContext(User usuario, boolean secure){
this.usuario = usuario;
this.secure = secure;
}
@Override
public String getAuthenticationScheme() {
return SecurityContext.FORM_AUTH;
}
@Override
public Principal getUserPrincipal() {
return usuario;
}
@Override
public boolean isSecure() {
return secure;
}
@Override
public boolean isUserInRole(String rol) {
if (usuario.getRoles() != null) {
return usuario.getRoles().contains(rol);
}
return false;
}
}
Podemos ver que nuestra clase de contexto de seguridad almacena el bean con el usuario logado que hemos creado anteriormente. El atributo “secure” es un booleano que indica si el acceso es por https.
Modificar el filtro de seguridad para crear el contexto de seguridad en cada petición
Ahora debemos modificar la clase RestSecurityFilter que creamos en el ejemplo de securización. Crearemos una instancia del contexto de seguridad (MyApplicationSecurityContext) y la añadiremos al contexto de las peticiones (ContainerRequestContext) para que esté accesible en nuestros métodos y pueda actuar correctamente la anotación @RolesAllowed.
@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
Jws claims = Jwts.parser().setSigningKey(KEY).parseClaimsJws(token);
//Creamos el usuario a partir de la información del token
User usuario = new User();
usuario.setUsername(claims.getBody().getSubject());
String roles = (String) claims.getBody().get("roles");
usuario.setRoles(Arrays.asList(roles.split(",")));
// Creamos el SecurityContext
MyApplicationSecurityContext secContext = new MyApplicationSecurityContext(usuario, requestContext.getSecurityContext().isSecure());
//Seteamos el contexto de seguridad
requestContext.setSecurityContext(secContext);
} catch (Exception e) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
}
}
}
Como podemos ver en los comentarios, estamos utilizando la información que viaja en el token para crear el usuario logado y el contexto de seguridad con los roles.
Registrar RolesAllowedDynamicFeature (Jersey)
En nuestro ejemplo estamos utilizando Jersey como implementación de JAX-RS. Para que funcionen las anotaciones @RolesAllowed en los métodos, debemos registrar la funcionalidad RolesAllowedDynamicFeature . Lo haremos en la clase de creación aplicación REST:
@ApplicationPath("rest")
public class Application extends ResourceConfig {
public Application() {
packages("es.infointernet.rest");
register(JacksonFeature.class);
register(RolesAllowedDynamicFeature.class);
}
}
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.