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:
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.
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
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?
ResponderEliminarGracias, un saludo.
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.
EliminarHola Carmelo muy buen tutorial, una consulta como configuras para validar token tu sabes para el csrf ? Lo estoy haciendo con angularjs, Gracias de antemano.
ResponderEliminarDe donde sacas la clase UserDetails y que tiene dentro?
ResponderEliminarEsta clase pertenece al framework spring en ella se deben almacenar los datos de autenticación de usuario: username, password, etc.
EliminarMe 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