# Tutorial Spring Boot Security - Parte 1: El Portero Inteligente!!!
Tabla de Contenidos
Muchachones, si están construyendo aplicaciones web con Spring Boot, tarde o temprano se van a topar con la necesidad de controlar quién entra y quién no. Ahí es donde entra Spring Security, el framework estrella de Spring para todo lo relacionado con seguridad: autenticación (quién eres?) y autorización (qué puedes hacer?).
Lo Mágico (y Peligroso) del Inicio:
Lo primero, mi bro, que tienes que saber es que Spring Security es medio intenso!!! Apenas agregas la dependencia * spring-boot-starter-security* a tu proyecto… De una!!! 💥 Automáticamente se vuelve loco y asegura TODA tu aplicación.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>Si levantas la app justo después de añadir esto, sin tocar nada más, notarás dos cosas:
-
Cualquier intento de acceder a un endpoint te lanza 401.
-
En la consola donde arrancaste la aplicación, verás un mensaje medio escondido que dice algo como: Using generated security password: [una clave larguísima]
Spring Security, en su deber de ser seguro desde el minuto cero, crea un usuario por defecto llamado user y le genera esa contraseña aleatoria cada vez que arranca. Puedes usar esas credenciales para autenticarte cuando pruebes un endpoint (con Postman o cualquier otra herramienta).
El Problema? Obviamente, esto no sirve para una aplicación real. Nadie quiere una contraseña que cambia cada vez, ni un solo usuario llamado “user” jajaja, ni una página de login fea. Además, queremos que los usuarios vengan de nuestra base de datos!!!
Agarra tu cafecito (o cervecita si eres de los mios), porque vamos a tomar el control. Le diremos a Spring Security: “Gracias por la ayuda inicial, pero ahora yo mando aquí bro!!!”. Vamos a configurar nuestro propio mecanismo de autenticación usando usuarios y contraseñas almacenados de forma segura en nuestra base de datos.
Conceptos Clave
Estos conceptos son la base y siguen siendo igual de importantes en Spring Security 6+:
-
SecurityFilterChain: La cadena de filtros de seguridad. Es el corazón de la configuración de seguridad web.
-
AuthenticationManager: El coordinador que delega la autenticación a los proveedores.
-
AuthenticationProvider: El especialista que hace la validación (usualmente DaoAuthenticationProvider para DB).
-
UserDetailsService: Tu implementación para buscar usuarios en la base de datos.
-
PasswordEncoder: Esencial para codificar y verificar contraseñas de forma segura (seguimos con * BCryptPasswordEncoder* como el estándar).
-
Database: Donde viven tus usuarios.
-
SecurityContextHolder: Donde se guarda la información del usuario autenticado durante su sesión.
Paso a Paso: Seguridad con Base de Datos (Spring Boot 3 / Security 6+)
Paso 1: Dependencias
Asumimos un pom.xml con al menos:
-
spring-boot-starter-web -
spring-boot-starter-security -
spring-boot-starter-data-jpa -
Driver de tu base de datos (PostgreSQL, MySQL, etc.)
-
jakarta.validation
Paso 2: Entidad User
import jakarta.persistence.*;import jakarta.validation.constraints.NotEmpty;
import java.util.Set;
@Entity@Table(name = "users")public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(nullable = false, unique = true) @NotEmpty private String username;
@Column(nullable = false) @NotEmpty private String password; // ¡Hasheado!
@ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) @Column(name = "role") private Set<String> roles; // Ejemplo: "ADMIN", "USER"
// Getters, setters, constructores...}Nota: @NotEmpty existe en Jakarta Validation 3.x, así que está bien usarlo.
Paso 3: Repositorio UserRepository
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username);}Paso 4: PasswordEncoder Bean
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;
@Configurationpublic class AppConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }}Paso 5: UserDetailsService Personalizado
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Service;
import java.util.Collection;import java.util.stream.Collectors;
@Servicepublic class CustomUserDetailsService implements UserDetailsService {
@Autowired private UserRepository userRepository;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado: " + username));
Collection<? extends GrantedAuthority> authorities = user.getRoles().stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) .collect(Collectors.toSet());
return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), true, true, true, true, authorities ); }}OJO con el prefijo ‘ROLE_’ si vas a usar .hasRole(“ADMIN”) en la configuración.
Paso 6: Configuración de Seguridad (SecurityFilterChain)
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.web.SecurityFilterChain;import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import static org.springframework.security.config.Customizer.withDefaults;
@Configurationpublic class SecurityConfig {
@Autowired private CustomUserDetailsService customUserDetailsService;
@Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/css/**", "/js/**", "/images/**", "/", "/home", "/register", "/error").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/profile/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .loginProcessingUrl("/login") .defaultSuccessUrl("/welcome", true) .failureUrl("/login?error=true") .permitAll() ) .logout(logout -> logout .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST")) .logoutSuccessUrl("/login?logout=true") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .permitAll() ) // Registramos nuestro UserDetailsService para DaoAuthenticationProvider .userDetailsService(customUserDetailsService) // Opciones adicionales .csrf(withDefaults()) .sessionManagement(withDefaults()) // Por defecto: session-fixation=migrateSession .exceptionHandling(ex -> ex .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) .accessDeniedPage("/access-denied") );
return http.build(); }}Detalles extra
Logout con POST: Es la práctica recomendada para evitar CSRF en logout.
CSRF y Sesiones: withDefaults() habilita las protecciones por defecto.
Session Fixation: El comportamiento por defecto en Spring Security 6 es migrateSession(), que es lo que queremos para seguridad.
Paso 7: Codificar Contraseña al Guardar
@Servicepublic class UserService { @Autowired private UserRepository userRepository; @Autowired private PasswordEncoder passwordEncoder;
public User registerNewUser(UserRegistrationDto dto) { User newUser = new User(); // … setear username, otros campos … newUser.setPassword(passwordEncoder.encode(dto.getPassword())); // ¡SIEMPRE! newUser.setRoles(Set.of("USER")); return userRepository.save(newUser); }}Cómo Funciona Todo Junto? (Flujo v6+)
-
Petición POST /login: Llega el username y password en el cuerpo de la petición.
-
‘SecurityFilterChain’ intercepta: ‘UsernamePasswordAuthenticationFilter’ detecta la URL y método.
-
Crea ‘UsernamePasswordAuthenticationToken’ (no autenticado).
-
Pasa al ‘AuthenticationManager’ (ProviderManager).
-
‘DaoAuthenticationProvider’ entra en acción:
-
Llama a ‘loadUserByUsername()’ de tu ‘CustomUserDetailsService’.
-
Si no existe, lanza ‘UsernameNotFoundException’.
-
Si existe, recupera la entidad y convierte roles en ‘GrantedAuthority’.
-
Verifica contraseña con ‘passwordEncoder.matches()’.
-
-
Si coincide, crea un nuevo ‘UsernamePasswordAuthenticationToken’ (autenticado) con el ‘UserDetails’.
-
Guarda ese token en ‘SecurityContextHolder’.
-
‘SessionManagementFilter’ migra la sesión (prevención de fijación).
-
Redirige a ‘/welcome’ (según tu config).
-
En caso de fallo, redirige a ‘/login?error=true’.
Y así, de sencillo panitas, tienen un portero de lujo protegiendo su aplicación con usuarios de base de datos y contraseñas seguras!!! En la proxima entrega abordaremos JWT.