Introduccion
Vamos a continuar/ampliar el anterior post que os dejo aqui.
En la anterior entrada vimos la manera, de todas las posibles soluciones que puedes tener con Spring Security, de poder tener una arquitectura de microservicios, haciendo que uno de esos microservicios sea unod e Authentication, mediante una conexión en BD y posterior validación de las peticiones REST que se generen a través de un filter que haga precisamente eso, un filtro que extraiga la cabecera, recupere el token y lo valide.
Ahora ampliamos esa primera entrega, con la introdución a la autorización (¿A qué tenemos persmiso para acceder?) y lo resolvemos con los ROLES.
Como siempre os dejo el acceso al código de la aplicación desde mi GitHub.
Algo de teoría
Aunque no es mi fuerte, y no me gusta, pero algo hay que tener claro antes de empezar.
Spring Security nos permite proteger nuestros endpoints con las etiquetas @PreAuthorize y @Secured indicando los roles que tienen acceso.
Podéis suponer que no son lo mismo, aunque comparten cosas:
- @PreAuthorize es una anotación más nueva que @Secured también es más flexible.
- @PreAuthorize soporta Spring Expression Language (SpEL) pudiendo utilizar expresiones como hasRole, permitAl.
- Con @Secured solo podemos referencia los ROLES permitidos.
- @Secured cuando se tienen varios roles en la lista que le pasemos se va a comportar como un OR. Con @PreAuthorize, admite expresiones (pudiendo usar AND, OR, NOT). Ejemplo:
- @Secured({«ROLE_ADMIN», «ROLE_USER»}) Acceso a todos usuarios con rol admin O user.
- @PreAuthorize(«hasRole(‘ROLE_ADMIN’) AND hasRole(‘ROLE_USER’)») Acceso a todos usuarios con rol admin Y user.
- @Secured({«ROLE_ADMIN»}) y @PreAuthorize(«hasRole(‘ROLE_ADMIN’)») son equivalentes.
A codificar!
Controller securizado
Vamos a añadir un controller (a modo de prueba) para saber cómo se debe securizar con lo explicado antes.
/** * UserController */ @RestController public class UserController { @PreAuthorize("hasRole('ROLE_USER') OR hasRole('ROLE_ADMIN')") @GetMapping("/userSecured") public String listUsersSecured() { return "listUsersSecured User or Admin"; } @PreAuthorize("hasRole('ROLE_ADMIN')") @GetMapping("/userSecuredAdmin") public String listUsersSecuredAdmin() { return "listUsersSecured Admin"; } @GetMapping("/users") public String listUsers() { return "listUsers"; } @PreAuthorize("hasRole('ROLE_SVC')") @GetMapping("/userService") public String listUsersService() { return "listUsersService"; } }
Podemos ver 3 Endpoints, uno dice que puedes acceder si tienes el ROLE_USER o ROLE_ADMIN. El segundo solo para ROLE_ADMIN y el ultimo es para una supuesta llamada interna entre microservicios ROLE_SVC.
Cambios en el modelo
Ahora vamos a tener que hacer algunos cambios para que la tabla USER y una futura tabla ROLES las relacionemos. Esa relacion es N a N, porque un usuario podrá tener N roles, pero esos roles pueden estar presentes en N usuarios.
Por lo que nos sale una tercera tabla para relacionarlo, pero por suerte eso lo hace spring por nosotros.
* No voy a profundizar en JPA de cómo sería hacerlo a bajo nivel.
ROLES
@Entity public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column private String name; @Column private String description; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Role(String name, String description) { this.name = name; this.description = description; } public Role() { } }
USERS
@Entity public class Usuario { @Id @GeneratedValue private long id; public String username; public String password; @ManyToMany(fetch = FetchType.EAGER, cascade = { CascadeType.MERGE, CascadeType.REFRESH }) @JoinTable(name = "USER_ROLES", joinColumns = { @JoinColumn(name = "USER_ID") }, inverseJoinColumns = { @JoinColumn(name = "ROLE_ID") }) private Setroles; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "Usuario [id=" + id + ", password=" + password + ", username=" + username + "]"; } public Usuario() { } public long getId() { return id; } public void setId(long id) { this.id = id; } public Usuario(String username, String password) { this.username = username; this.password = password; } public Set getRoles() { return roles; } public void setRoles(Set roles) { this.roles = roles; } public Usuario(String username, String password, Set roles) { this.username = username; this.password = password; this.roles = roles; } }
Podemos ver el cambio desde la tabla de usuario, para poder obtener de un usuario los ROLES asociados que tiene, que es lo que verdaderamente nos importa.
Luego por ejemplo para dar de alta un usuario haremos algo del estilo
Role role1 = new Role("ROLE_ADMIN", "Admin role"); roleRepo.save(role1); Role role2 = new Role("ROLE_USER", "User role"); roleRepo.save(role2); Role role3 = new Role("ROLE_SVC", "Service role"); roleRepo.save(role3); Setroles = new HashSet<>(); roles.add(role2); Usuario usuario1 = new Usuario("usuario", bCryptPasswordEncoder.encode("usuario"), roles); // Solo user userRepo.save(usuario1); roles.add(role1); Usuario usuario2 = new Usuario("admin", bCryptPasswordEncoder.encode("admin"),roles); // User & Admin userRepo.save(usuario2); roles.clear(); roles.add(role3); Usuario usuario3 = new Usuario("service", bCryptPasswordEncoder.encode("service"),roles); userRepo.save(usuario3);
Configuramos
La parte de la configuración es sencilla, realmente la podríamos haber tenido activada sin que por ello la aplicación no fuese 100% funcional.
@Configuration // @EnableGlobalMethodSecurity(securedEnabled = true) // Este es el método 1 de // securización a usar: @Secured @EnableGlobalMethodSecurity(prePostEnabled = true) // Este es el método 2 de securización a usar: @PreAuthorize public class SecurityConfig extends WebSecurityConfigurerAdapter { ..... }
Modificamos el Token
Para que con el token viaje el ROL debemos hacer un pequeño cambio. Aquí la variedad de soluciones es amplia, pero vamos a modificar la interfaz de UserDetails, para una vez obtenido el usuario de la BBDD podamos añadir la información correspondiente a los permisos.
Vamos a cambiar esta parte:
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Usuario usuario = usuarioRepository.findByUsername(username); if (usuario == null) { throw new UsernameNotFoundException(username); } return UserDetailsMapper.build(usuario); }
Vemos que aparece un UserDetailsMapper.
/** * UserDetailsMapper */ public class UserDetailsMapper { public static UserDetails build(Usuario user) { return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthorities(user)); } private static Set getAuthorities(Usuario retrievedUser) { Setroles = retrievedUser.getRoles(); Set authorities = new HashSet<>(); roles.forEach(role -> authorities.add(new SimpleGrantedAuthority( role.getName()))); return authorities; } }
Como vemos la parte importante está en :
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),getAuthorities(user));
Ese ultimo parámetro antes lo mandabamos vacío, porque todos tenían el mismo ROL, y ahora ya no, por lo que debemos sacar la lista de ROLES.
¿Y qué añadimos al TOKEN?
Pues tampoco tanto.
@Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication auth) throws IOException, ServletException { final String authorities = auth.getAuthorities().stream().map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); String token = Jwts.builder() .setIssuedAt(new Date()) .setIssuer(SecurityConstants.ISSUER_INFO) .setSubject(((User) auth.getPrincipal()).getUsername()) .claim(SecurityConstants.AUTHORITIES_KEY, authorities) .setExpiration(new Date(System.currentTimeMillis() + SecurityConstants.TOKEN_EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS512, SecurityConstants.SUPER_SECRET_KEY) .compact(); response.addHeader(SecurityConstants.HEADER_AUTHORIZACION_KEY, SecurityConstants.TOKEN_BEARER_PREFIX + " " + token); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write("{\"" + SecurityConstants.TOKEN_PREFIX + "\":\"" + token + "\"}"); }
Vemos como hemos añadido la parte de .claim(SecurityConstants.AUTHORITIES_KEY, authorities)
Con la authorities que ahora sí, las hemos obtenido desde nuestro usuario.
¿Y para validarlo?
recordamos que teníamos otro filtro, que hacía eso precisamente. Cuando nos llegue un TOKEN, lo "ABRIMOS", buscamos la parte donde viaja el ROL y a security debemos pasarle los authorities. Pues añadimos esta parte:
if (user != null) { final Collectionauthorities = Arrays .stream(claims.get(SecurityConstants.AUTHORITIES_KEY).toString().split(",")).map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return new UsernamePasswordAuthenticationToken(user, "", authorities); }
Resumen
Como hemos visto, lo que le interesa a Security para poder authorizar son los authorities (permisos) que tiene ese asignados esa petición de recurso y que hemos decidido que viaje en el token.Se podría por ejemplo que viaje solo el username, por ejemplo, se valida el token y si es válido, en otro servicio se le podrían buscar los permisos y que no viaje en el token...
Las configuraciones son variadas. Aquí solo expongo una de las tantas formas que me parece más clara para poder tenerlo en una arquitectura de microservicios.
Espero que sirva de ayuda y aclare alguna que otra duda.
No hay comentarios:
Publicar un comentario
Nota: solo los miembros de este blog pueden publicar comentarios.