Login de Usuario y SpringBoot

Otra de esas entradas para mi, pero que puede servirle a alguien.

En las «cosas» que había escrito en SpringBoot, hasta ahora, no necesité crear usuarios ni darle permisos a estos.

Leyendo por aquí y por allá, logré agregarle un login y permisos a ciertas acciones.

Ok, lo primero es agregar la dependencia para el manejo de usuario. En Initializr lo encontramos por Spring Security. Ya sea por Gradle o Maven: ‘org.springframework.boot:spring-boot-starter-security’.

Recargamos y listoco.

Modelo

Ahora seguimos con la clase para nuestro usuario, y otra para los roles. En este tuto, y básicamente porque en mi caso no necesito más, manejaré un rol por usuario. No más. Quiero decir, el usuario será Administrador o Miembro. No tendrá más de un rol.

De esta forma, la clase miembro quedaría:

@Entity
@Table(name = "usuario", uniqueConstraints = @UniqueConstraint(columnNames = "nombre"))
public class UsuarioModel {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Long id;

    @Column(name = "nombre")
    private String nombre;
    private String password;

    @ManyToOne
    public RolModel rol;
}

Importante destacar que la columna nombre será única. Y que el rol quede marcado @ManyToOne (un rol -> muchos usuarios). Agregamos los getters/setter/constructores y eso sería.

Respecto a la clase Rol:

@Entity
@Table(name = "rol")
public class RolModel {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String nombre;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name="rol_id")
    private Set<UsuarioModel> usuarios;
}

Aquí es donde definimos la relación desde usuario. De esta forma, la tabla usuario queda como:

create table usuario
(
    id       bigserial
        primary key,
    nombre   varchar(255)
        constraint ukcto7dkti4t38iq8r4cqesbd8k
            unique,
    password varchar(255),
    rol_id   bigint
        constraint fkshkwj12wg6vkm6iuwhvcfpct8
            references rol
);

alter table usuario
    owner to montesanto;

Ojo que es el código que genera Hibernate. Además, estoy usando PostgreSQL.

Repositorio

Aquí es nada de otro mundo. Simplemente:

@Repository
public interface UsuarioRepository extends CrudRepository<UsuarioModel, Long> {

    public UsuarioModel findByNombre(String nombre);
}

Y podemos definir todas las funciones extras que necesitemos.

Servicio

Aquí cambia un poco, básicamente porque dividimos el servicio para Usuario en 2. Esto porque nuestro servicio tendrá que heredar funciones de la clase org.springframework.security.core.userdetails.UserDetailsService

UsuarioService

public interface UsuarioService extends UserDetailsService { }

Ojo que no está marcada como Servicio. Eso queda para la próxima clase. Podríamos definir las típicas funciones de guardar, o listar. O las podríamos dejar en la implementación de esta clase. Al menos yo lo dejé así (en la clase que implementa) para no escribir 2 veces, aunque puede no ser ordenado.

UsuarioServiceImpl

Esta clase es larga, así que iremos por parte.

La definimos como:

@Service
public class UsuarioServiceImpl implements UsuarioService{

    private final UsuarioRepository usuarioRepository;

    public UsuarioServiceImpl(UsuarioRepository usuarioRepository){
        this.usuarioRepository = usuarioRepository;
    }
}

Aquí mismo agregamos el método para registrar nuevos usuarios:

public void guardar(UsuarioModel usuarioModel){
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    usuarioModel.setPassword(passwordEncoder.encode(usuarioModel.getPassword()));
    usuarioRepository.save(usuarioModel);
}

Lo relevante aquí es la clave. Usamos la clase org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder para encriptarla.

Respecto al login y acceso en general, es obligatorio agregar el siguiente método:

@Override
public UserDetails loadUserByUsername(String nombre) throws UsernameNotFoundException {
    UsuarioModel usuarioModel = usuarioRepository.findByNombre(nombre);
    if(usuarioModel == null){
        System.out.println("Usuario o password incorrectos");
        throw new UsernameNotFoundException("Usuario o password incorrectos");
    }
    return new User(usuarioModel.getNombre(), usuarioModel.getPassword(), mapearAutoridadesRoles(usuarioModel.getRol()));
}

Este método lo utiliza automágicamente Spring al loguear un usuario. Lo importante es notar que devuelve un org.springframework.security.core.userdetails.User. El constructor de dicha clase necesita un nombre de usuario, una contraseña (contraseña que ya quedó encriptada en BD) y un arreglo con todos los roles que tenga el usuario.

Como ya dije, en mi caso únicamente tengo (o al menos por ahora) un único rol por usuario, pero como necesito un listado, lo dejé así para transformar más fácil:

private Collection<? extends GrantedAuthority> mapearAutoridadesRoles(RolModel rol){
    return Collections.singleton(new SimpleGrantedAuthority(rol.getNombre()));
}

Controlador

Es como cualquier otro… Tal vez lo único relevante es que al momento de registrar un usuario, agregué el rol a «mano». Ya que los administradores los fijaré yo mismo por BD.

@Controller
public class UsuarioController {

    @Autowired
    UsuarioServiceImpl usuarioService;

    @PostMapping(value = "/Usuario/Nuevo")
    public String nuevo(@Valid @ModelAttribute("formData") UsuarioFormData formData,
                        BindingResult binding,
                        Model model){
        if(binding.hasErrors()){
            return "usuarios/nuevo";
        }
        try {
            UsuarioModel usuario = formData.toModel();
            usuario.setRol(new RolModel(2L,"Miembro"));
            usuarioService.guardar(usuario);
            return "redirect:/Usuarios";
        } catch (Exception e) {
            System.out.println(e.getMessage());
            binding.rejectValue("nombre", "error.user", "Nombre de usuario ya existe");
            return "usuarios/nuevo";
        }
    }
}

Clase SecurityConfig

Esta clase se encarga de todo:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UsuarioService usuarioService;

    @Bean
    BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider(){
        DaoAuthenticationProvider auth = new DaoAuthenticationProvider();
        auth.setUserDetailsService(usuarioService);
        auth.setPasswordEncoder(passwordEncoder());
        return auth;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }
}

Hereda de org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter.

Obviamente, necesitamos injectar nuestro servicio (ojo que estamos llamando al «servicio en blanco»)

El primer método se encarga de asociar tanto el método para obtener el usuario (desde BD, en nuestro caso) como fijar la forma en que encriptamos la contraseña (o desencriptamos, en este caso).

Y el segundo lo agrega al adapter.

Esta clase tiene un tercer método que veremos ahora:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/css/**", "/js/**", "/img/persona/**", "/login", "/Usuario/Nuevo").permitAll()
        .antMatchers("/").permitAll()
        .antMatchers("/Himnario", "/Cancion/Etiquetas/**", "/Cancion/Nro/**", "/Cancion/Busqueda").permitAll()
        .antMatchers("/Biblia", "/Biblia/Busqueda", "/Libro/**").permitAll()
        .antMatchers("/Articulos", "/Articulo/Busqueda", "/Articulo/Nro/**").permitAll()
        .antMatchers("/Cancion/Nueva", "/Cancion/Editar/**", "/Articulo/Nuevo").hasAuthority("Administrador")
        .antMatchers("/**").hasAnyAuthority("Administrador", "Miembro")
        .anyRequest().authenticated()
        .and()
            .formLogin()
            .loginPage("/login")
            .defaultSuccessUrl("/escritorio")
            .failureUrl("/login?error=Usuario o Clave incorrecta")
        .and()
            .logout()
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            .logoutSuccessUrl("/login")
            .invalidateHttpSession(true)
            .clearAuthentication(true)
            .permitAll();
}

Como ven, aquí definimos cuales rutas serán «públicas» y cuáles necesitarán un usuario logueado al sistema.

El método antMatchers() acepta las rutas y luego le agregamos si permitirá o no un acceso. O si lo bloqueará. Y en el caso de bloquear, cuál authority debe tener el miembro.

El orden siempre es decendente. Lo que se defina primero quedará fijo. No es como un css que define el último.

Luego agregamos la ruta del formulario de login, dónde iremos en caso de loguearnos correctamente o dónde ir en caso contrario.

Lo mismo para el logout.

Ahora que lo pienso, hubiese sido mucho más simple agregar las rutas como /admin/Cancion/Nueva, o algo así… en ese caso directamente podría ser .antMatchers(«/admin/**») sin necesidad de marcar todo.

Ok, tenemos todo. Para terminar podemos fijar en Thymeleaf permisos dependiendo de tal o cuál rol tiene cierto usuario.

Primero, agregamos la dependencia:

'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.0.4.RELEASE'

En mi caso tuve que agregar la versión…

Ahora, para que el IDE que usemos identifique las etiquetas, agregamos:

<!doctype html>
<html lang="es" class="has-navbar-fixed-top"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

Y ahora tenemos acceso a, por ejemplo:

<div sec:authentication="name">
    The value of the "name" property of the authentication object should appear here.
</div>
<div sec:authorize="hasAuthority('Administrador')">
    This will only be displayed if authenticated user has auth admin.
</div>
<div sec:authorize="hasAuthority('Miembro')">
    This will only be displayed if authenticated user has auth miembro.
</div>
<div sec:authorize="isAuthenticated()">
    Text visible only to authenticated users.
</div>

Y eso sería todo!

Ahora les queda probar 😉

Links

Acerca de MaritoCares

Ingeniero Informático. Con tendencias a la programación en [C#, VB].NET, Java(Web principalmente...), PHP, JavaScript, algo mínimo de [ruby, python], y el clásico C.
Esta entrada fue publicada en Java, PostgreSQL, Tutoriales y etiquetada . Guarda el enlace permanente.

2 respuestas a Login de Usuario y SpringBoot

  1. Anónimo dijo:

    que es UsuarioFormData?

    • MaritoCares dijo:

      Hola!
      Tienes toda la razón … no lo especifiqué en el post…
      UsuarioFormData es el DTO de entrada. Es una clase POJO que trae los «campos» en tu página y finalmente la conviertes a lo que necesitas en tu modelo.

Deja un comentario