27 jun 2019

Securizando APIs (II) [Roles]: SpringBoot + Security + JWT + Auth en Base de datos

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 Set roles;

    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);

   Set roles = 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) {
        Set roles = 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 Collection authorities = 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.

Share This!



No hay comentarios:

Publicar un comentario

Nota: solo los miembros de este blog pueden publicar comentarios.