Guía para el uso del módulo de Spring Security en Spring Boot
Para poder seguir de la mejor manera esta guía, te recomiendo empezar con el proyecto de Spring Boot que se encuentra en este enlace: Enlace de repositorio antes de Security A pesar de ello, esta guía podría serte útil para la configuración de Spring Security en cualquier proyecto de Spring Boot.
Activando Spring Security
- Para iniciar con Spring Security, es necesario agregar al
pom.xmldel proyecto el siguiente código:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
-
Ejecuta el comando
mvn clean installpara instalar las dependencias. -
Ejecuta el comando
mvn spring-boot:runpara correr la aplicación. A partir de agregar la dependencia de Spring Security, la aplicación se bloqueará ante cualquier solicitud y pedirá un usuario y contraseña. Por defecto, Spring Security crea un usuario llamadousery una contraseña aleatoria que se muestra en la consola al iniciar la aplicación. -
Intenta hacer una solicitud para obtener la información de la aplicación, por ejemplo, intenta ingresar a
http://localhost:8081/compunet2-2025/mvc/users. Spring Security bloqueará la solicitud y pedirá un usuario y contraseña.4.1 Si estás usando Postman, puedes agregar un usuario y contraseña en la pestaña de
Authorization. Selecciona el tipoBasic Authy agrega el usuario y contraseña que se muestra en la consola al iniciar la aplicación. -
Para poder modificar este usuario y contraseña, lo más adecuado será crear un archivo de configuración (anotado con
@Configuration) y crear un bean de tipoUserDetailsService. En este bean, se puede modificar el usuario y contraseña por defecto.
package com.games.back.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class WebSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsMngr = new InMemoryUserDetailsManager();
UserDetails user = User.withUsername("miUsuario") // Cambiar el usuario
.password("123456") // Especificar la contraseña
.authorities("read") // Las authorities representan los permisos que tiene el usuario
.roles("USER") // Los roles son un conjunto de authorities
.build();
userDetailsMngr.createUser(user); // Agregar el usuario a la lista de usuarios
return userDetailsMngr; // Retornar la lista de usuarios
}
}
- Además, es importante especificar un
PasswordEncoderpara que Spring Security pueda encriptar la contraseña. Se puede hacer de la siguiente manera:
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); // No usar para producción
// return new BCryptPasswordEncoder(); // Más recomendable y generalmente usado, no usar si aún no has hecho el proceso de encriptación
}
- Si vuelves a ejecutar la aplicación, podrás ver que el usuario y contraseña por defecto ya no funcionan (además de que no aparece la contraseña autogenerada) y que ahora debes usar el usuario y contraseña que especificaste en el archivo de configuración.
Custom UserDetailsService
Hasta el momento hemos visto cómo crear un usuario en memoria, pero en la mayoría de los casos, los usuarios y contraseñas se almacenan en una base de datos. Para ello, es necesario crear una clase que implemente la interfaz UserDetailsService y sobreescribir el método loadUserByUsername.
- Crea una carpeta en
servicepara los aspectos de autenticación, y ahí crea elCustomDetailsService. Este nos permitirá buscar un usuario en la base de datos por su nombre de usuario y no quemar el usuario y contraseña en el código.
public class CustomUserDetailsService implements UserDetailsService{
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'loadUserByUsername'");
}
}
- Además de eso, ya podemos modificar nuestro bean de
UserDetailsServicepara que use elCustomUserDetailsService. (O anotarlo con@Service).
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
- Es importante tener lista nuestra entidad de usuarios hasta este punto con sus repositorios y servicios creados. Añadiremos un nuevo servicio que permita buscar un usuario por su nombre de usuario (si aún no ha sido especificado).
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User findByUsername(String username) {
return userRepository.findByUsername(username);
}
}
- Debemos inyectar la dependencia de
UserServiceen nuestroCustomUserDetailsServicepara poder buscar el usuario por su nombre de usuario.
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
return null;
}
}
- Hasta aquí esto está incompleto debido a que el método
loadUserByUsernamedebe retornar un objeto de tipoUserDetails. Para solucionar esto, debemos crear una clase que implemente la interfazUserDetailsy retornar una instancia de esta clase. Recomiendo entonces crear una carpetasecurityy crear ahí la claseCustomUserDetails.
package com.games.back.security;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.games.back.model.User;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {
private final User user;
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// GrantedAuthority es una interfaz que representa un permiso concedido a un objeto de autenticación.
// Podemos crear una implementación personalizada de GrantedAuthority para representar nuestros propios permisos.
// En este caso, estamos devolviendo una lista de permisos que el usuario tiene.
return List.of(() -> "read");
}
}
- Ahora, en nuestro
CustomUserDetailsServiceya podremos retornar una instancia deSecurityUseren el métodoloadUserByUsername.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
User user = userService.findByUsername(username);
return new CustomUserDetails(user);
} catch (RuntimeException ex) {
throw new UsernameNotFoundException("User not found with username: " + username, ex);
}
}
- Intentemos ejecutar la aplicación nuevamente y ver si podemos autenticarnos con el usuario y contraseña que hemos especificado en la base de datos.
Custom SecurityAuthorities
- Hasta el momento se ha dejado quemado el permiso del usuario en el método
getAuthoritiesde la claseSecurityUser. Para poder hacer esto de manera más dinámica, se puede crear una clase que implemente la interfazGrantedAuthorityy retornar una lista de estas instancias en el métodogetAuthorities.
package com.games.back.security;
import org.springframework.security.core.GrantedAuthority;
import com.games.back.model.Permission;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class SecurityAuthority implements GrantedAuthority {
private final Permission permission;
@Override
public String getAuthority() {
return permission.getName();
}
}
// En el método getAuthorities de SecurityUser
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SecurityAuthority> authorities = user.getRole().getRolePermissions().stream()
.map(RolePermission::getPermission)
.map(SecurityAuthority::new)
.toList();
return authorities;
}
Nota: Aquí lanzará un error respecto al Lazy Loading, ya que estamos intentando obtener algo fuera del contexto de Hibernate. Por favor, soluciona este problema.
- Después de realizar todo hasta este punto, ya deberías poder autenticarte con un usuario y contraseña que se encuentre en la base de datos. Pero además de eso, imprime antes de enviar la respuesta al usuario las authorities, y comprueba que tenga todas las authorities asignadas a ese usuario:
// En el controlador de usuarios
@GetMapping
public String getAll(Model model) {
model.addAttribute("users", userService.findAll());
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
auth.getAuthorities().forEach(authority -> System.out.println(authority.getAuthority()));
return "users/list";
}
Contraseñas hasheadas
Para poder usar contraseñas hasheadas, es necesario cambiar el PasswordEncoder en el archivo de configuración de seguridad. En lugar de usar NoOpPasswordEncoder, se puede usar BCryptPasswordEncoder.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
No obstante, puede que empiece a fallar la autenticación si las contraseñas en la base de datos no están hasheadas. Para solucionar esto, es necesario hashear las contraseñas antes de guardarlas en la base de datos. Esto se puede hacer en el servicio de usuarios.
@Service
public class UserService {
@Autowired
private PasswordEncoder passwordEncoder;
public User save(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
}
Y en el arhivo data.sql, es necesario hashear las contraseñas antes de insertarlas en la base de datos. Puedes usar una herramienta en línea para hashear las contraseñas con BCrypt.
Las contraseñas serán password hasheada con BCrypt:
INSERT INTO users (username, email, password_hash, bio, created_at, role_id, birthdate) VALUES
('admin', 'admin@example.com', '$2y$10$6o5vS5YmB6/txDbxtABg8OlTI2XTrdzGdwwsOt4EgVRsJujeef6CC', 'Administrator account', CURRENT_TIMESTAMP, 1, '1980-01-01'),
Rutas privadas y públicas
Para manejar las rutas privadas y públicas, es necesario modificar el archivo de configuración de seguridad. En este archivo, se puede especificar qué rutas son públicas y cuáles requieren autenticación.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/mvc/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.logout(Customizer.withDefaults())
.build();
}
Esto permitirá el acceso a cualquier ruta que comience con /mvc/public/ sin necesidad de autenticación, mientras que cualquier otra ruta requerirá autenticación.
@Controller
@RequestMapping("/mvc/public")
public class PublicMVCController {
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}
También se agregó la funcionalidad de login y logout por defecto de Spring Security. De esta manera podrías agregar un enlace a /login en tu aplicación para que los usuarios puedan autenticarse así como un enlace a /logout para cerrar sesión. Aquí te presento un ejemplo:
<header th:fragment="header">
<h1>Gestión de Usuarios</h1>
<nav>
<ul>
<li><a th:href="@{/mvc/users}">Lista de Usuarios</a></li>
<li><a th:href="@{/mvc/users/add}">Agregar Usuario</a></li>
<li>
<form th:action="@{/logout}" method="post" style="display: inline;">
<button type="submit">Logout</button>
</form>
</li>
</ul>
</nav>
<hr>
</header>
Lógin personalizado
También tenemos la posibilidad de crear un formulario de login personalizado. Para ello, es necesario crear un controlador que maneje las rutas de login y logout.
@Controller
@RequestMapping("/mvc/auth")
public class LoginController {
@GetMapping("/login")
public String login() {
return "auth/login";
}
}
También deberemos de modificar nuestro filter chain para que use nuestro formulario de login personalizado.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/mvc/public/**").permitAll()
.requestMatchers("/mvc/auth/login", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/mvc/auth/login") // URL personalizada para mostrar login
.loginProcessingUrl("/mvc/auth/login") // URL que procesa el login
.defaultSuccessUrl("/mvc/users", true) // Redirección después del login exitoso
.failureUrl("/mvc/auth/login?error") // Redirección en caso de error
.usernameParameter("username") // Nombre del campo username
.passwordParameter("password") // Nombre del campo password
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/mvc/auth/login?logout")
.permitAll()
)
.build();
}
Por último, es necesario crear la vista del formulario de login personalizado.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" th:href="@{/css/auth/login.css}">
</head>
<body>
<div class="login-container">
<h2>Iniciar Sesión</h2>
<!-- Mensaje de error -->
<div th:if="${param.error}" class="error">
Usuario o contraseña incorrectos
</div>
<!-- Mensaje de logout -->
<div th:if="${param.logout}" class="success">
Has cerrado sesión correctamente
</div>
<form th:action="@{/mvc/auth/login}" method="post">
<div>
<label for="username">Usuario:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Contraseña:</label>
<input type="password" id="password" name="password" required>
</div>
<div>
<button type="submit">Iniciar Sesión</button>
</div>
</form>
</div>
</body>
</html>