Spring Security con Hibernate JPA

En tutoriales anteriores estudiamos el formulario de inicio de sesión, ahora, en este tutorial añadiremos la página de registro de usuarios, también aprovecharemos para ver como integrar la tecnología de persistencia Hibernate JPA a Spring Security, usaremos una base de datos HSQLDB para el proyecto, y además aprenderemos a utilizar la interface UserDetailsService para extraer los datos de autenticación y autorización desde los respectivos repositorios JPA.

Seguimos trabajando con el proyecto creado anteriormente, Spring Security integrar Base de Datos.

Integrar Hibernate JPA

Para integrar esta tecnología a nuestra aplicación web segura primero debemos agregar las dependencias necesarias, estas son:

<!-- Spring Data JPA -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>1.11.1.RELEASE</version>
</dependency>

<!-- Hibernate -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>4.3.11.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>4.3.11.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.3.4.Final</version>
</dependency>

Localizamos la clase WebDataConfig que es la encargada de configurar la fuente de datos, conexión y demás, aquí vamos a agregar la configuración necesaria para utilizar Hibernate JPA como proveedor de datos, anteriormente dedicamos un tutorial a esta tarea, puedes verlo en, Tutorial Spring Data JPA.

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories("carmelo.spring.data.repository")
@ComponentScan("carmelo.spring.data.service")
public class WebDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.HSQL)
                .generateUniqueName(true)
                .build();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
        factoryBean.setDataSource(dataSource());
        factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
        factoryBean.setPackagesToScan("carmelo.spring.data.model");
        return factoryBean;
    }

    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
        jpaVendorAdapter.setGenerateDdl(true);
        jpaVendorAdapter.setDatabase(Database.HSQL);
        return jpaVendorAdapter;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
        return transactionManager;
    }
}

Ahora podemos preparar nuestras entidades JPA, para nuestro caso crearemos la clase Usuario que nos permitirá almacenar los datos de cada usuario y nos servirá para autenticarlo.

@Data
@Entity
public class Usuario {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @NotEmpty(message = "Debe indicar el nombre del usuario.")
    private String nombre;

    @NotEmpty(message = "Debe indicar el apellido del usuario.")
    private String apellido;

    @NotEmpty(message = "Se requiere una contraseña para poder acceder.")
    private String password;

    @Email(message = "La dirección de correo electrónico es incorrecta.")
    private String email;

    @NotEmpty(message = "Seleccione los roles para el usuario.")
    @ElementCollection(fetch = FetchType.EAGER)
    private List<String> roles;

}

También requerimos agregar el repositorio JPA que nos permitirá lanzar consultas sobre esta entidad, esta es la siguiente:

public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
    Usuario findByNombre(String nombre);
}

La explicación ha sido breve ya que todo lo mencionado ha sido estudiado en tutoriales previos, puedes ver la lista de los mismos en: Tutoriales Spring Framework.

Configurar UserDatailsService

Para poder autenticar a los usuarios usando el repositorio que acabamos de crear es necesario indicarle a Spring Security como debe hacerlo, para ello debemos implementar la interface UserDetailsService.

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UsuarioRepository usuarios;

    @Override
    public UserDetails loadUserByUsername(String string) throws UsernameNotFoundException {

        Usuario usr = usuarios.findByNombre(string);
        
        List<SimpleGrantedAuthority> auths = usr.getRoles().stream()
                .map(rol -> new SimpleGrantedAuthority(rol))
                .collect(Collectors.toList());

        return new User(usr.getNombre(), usr.getPassword(), auths);
    }
}

Esta interface posee un único método, este es el encargado de obtener el usuario según el nombre indicado al momento de iniciar sesión, en nuestro caso se refiere al nombre de usuario, otra alternativa podría ser por correo, luego debemos crear un objeto User que es usado por Spring Security para autenticar y autorizar al usuario, debemos indicar el nombre, contraseña y la lista de roles que el mismo posee.

Para indicar al proveedor de seguridad que debe usar esta clase, vamos a la configuración de seguridad y hacemos lo siguiente:

@Configuration
@EnableWebSecurity
public class WebSecConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetails;
    
    @Autowired
    public void configureAuth(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetails);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception { ... }
}

Con esto ya podemos iniciar sesión usando el repositorio JPA mediante la interface UserDetailsService.

Formulario de registro de usuarios

Para poder iniciar sesión primero debemos tener usuarios en la base de datos, en tutoriales anteriores los agregamos manualmente, pero en una aplicación real lo común es que el usuario se registre, lo hacemos usando el siguiente controlador:

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UsuarioRepository usuarios;

    @RequestMapping(value = "/register", method = RequestMethod.GET)
    public String register(Model model) {
        model.addAttribute("usuario", new Usuario());
        return "usuario";
    }

    @RequestMapping(value = "/register", method = RequestMethod.POST)
    public String register(@Valid Usuario user, BindingResult result) {
        
        if(result.hasErrors()) {
            return "usuario";
        }
        
        usuarios.save(user);
        
        return "redirect:/";
    }

    @ModelAttribute("roles")
    public List<String> getRoles() {
        return Arrays.asList("ROLE_ADMIN", "ROLE_USER");
    }  
}

Este es un controlador para un formulario que implementa validación de datos, estamos aplicando lo aprendido, validación de formularios Spring MVC, la vista es generado usando el archivo usuario.jsp, el método getRoles() define la lista roles a los que un usuarios puede pertenecer.

La vista JSP es similar a la presentada en tutoriales anteriores, salvo que cambio el estilo y diseño de la misma, pero esto es solo aplicación de reglas CSS, el formulario de registro se ve de la siguiente forma:

Tutorial Spring Security Formulario de Registro de Usuarios

Es necesario recordar permitir el acceso a esta URL, antMatchers("/user/**").permitAll().

También agregamos un enlace en el formulario login.jsp que le permite al usuario ir rápidamente a la página de registro.

Spring Security Formulario de Login

Protección de las contraseñas

Un mecanismo de seguridad que hay que tener en cuenta a la hora de guardar los datos del usuario es proteger las contraseñas, usualmente no se almacena la contraseña como tal, lo que guardamos en la base de datos es el valor hash de las mismas, de este modo si alguien logra tener acceso a nuestra base de datos de usuarios no podrá leer las contraseñas.

Para esta tarea Spring Security utiliza la interface PasswordEncoder, podemos crear nuestra propia implementación o utilizar una de las proporcionadas por el Framework, para este ejemplo utilizaremos la clase BCryptPasswordEncoder, se utiliza del siguiente modo:

@Configuration
@EnableWebSecurity
public class WebSecConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetails;

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

    @Autowired
    public void configureAuth(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetails)
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception { ... }
}

Antes de guardar el registro de un nuevo usuario debemos recordar calcular el hash de la contraseña, por esto nos vamos al controlador respectivo y usamos el método encode() para calcular el valor hash de la contraseña introducida por el usuario.

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired private UsuarioRepository usuarios;
    
    @Autowired private PasswordEncoder encoder;

    @RequestMapping(value = "/register", method = RequestMethod.GET)
    public String register(Model model) { ... }

    @RequestMapping(value = "/register", method = RequestMethod.POST)
    public String register(@Valid Usuario user, BindingResult result) {
        
        if(result.hasErrors()) {
            return "usuario";
        }
        
        // calcular el HASH de la contraseña
        String password = encoder.encode(user.getPassword());
        user.setPassword(password);
        
        usuarios.save(user);
        
        return "redirect:/";
    }

    @ModelAttribute("roles") public List<String> getRoles() { ... }  
}

La clase BCryptPasswordEncoder, generará algo como esto:

$2a$10$fYolYBQcszFwBxwT3hjbj.QY6TDz1SvDDi9SQgY028maC6khfg2di

Cuando un usuario intente iniciar sesión se calcular el hash de la contraseña que el mismo introduzca y se compara con el hash almacenado en la base de datos. 

Este es un proyecto básico que aun tiene muchas cosas que mejorar, lo iremos haciendo a medida avancemos en el nivel de los tutoriales, de momento está bien para tener las bases y comprender como funciona esta tecnología, hasta la próxima.

Descargar código fuente: formulario de registro.zip

Comentarios

  1. Hola, ¿que tal? Gracias por el tutorial. Me surge una duda, ¿como recuperaría el usuario de la base de datos, ya que al encriptar la contraseña cuando la introduzco en un formulario de login busca un usuario con la contraseña sin encriptar. ¿Debería usar la función {encoder.encode (user.getPassword ())} en el formulario de login para comparar la contraseña encriptada?

    Gracias, un saludo.

    ResponderEliminar
    Respuestas
    1. Deberías buscar en la base de datos un usuario cuyo nombre y contraseña encriptada coincidan y si efectivamente debes usar la función que mencionas para obtener la contraseña que se encuentra en la base de datos.

      Eliminar
  2. Hola Carmelo muy buen tutorial, una consulta como configuras para validar token tu sabes para el csrf ? Lo estoy haciendo con angularjs, Gracias de antemano.

    ResponderEliminar
  3. De donde sacas la clase UserDetails y que tiene dentro?

    ResponderEliminar
    Respuestas
    1. Esta clase pertenece al framework spring en ella se deben almacenar los datos de autenticación de usuario: username, password, etc.

      Eliminar
  4. Me encanto todo tu contenido, le entendí mejor a todo. Una pregunta, hay manera lo mismo pero haciendo la conexión a la base de datos usando el hibernate.cfg.xml?

    ResponderEliminar

Publicar un comentario

Temas relacionados

Entradas populares de este blog

tkinter Grid

Controles y Contenedores JavaFX 8 - I

Conectar SQL Server con Java

tkinter Canvas