# Tutorial Spring Boot Security - Parte 1: El Portero Inteligente!!!

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:

  1. Cualquier intento de acceder a un endpoint te lanza 401.

  2. 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+:

  1. SecurityFilterChain: La cadena de filtros de seguridad. Es el corazón de la configuración de seguridad web.

  2. AuthenticationManager: El coordinador que delega la autenticación a los proveedores.

  3. AuthenticationProvider: El especialista que hace la validación (usualmente DaoAuthenticationProvider para DB).

  4. UserDetailsService: Tu implementación para buscar usuarios en la base de datos.

  5. PasswordEncoder: Esencial para codificar y verificar contraseñas de forma segura (seguimos con * BCryptPasswordEncoder* como el estándar).

  6. Database: Donde viven tus usuarios.

  7. SecurityContextHolder: Donde se guarda la información del usuario autenticado durante su sesión.

Diagrama Spring Security

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;
@Configuration
public 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;
@Service
public 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;
@Configuration
public 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

@Service
public 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+)

  1. Petición POST /login: Llega el username y password en el cuerpo de la petición.

  2. ‘SecurityFilterChain’ intercepta: ‘UsernamePasswordAuthenticationFilter’ detecta la URL y método.

  3. Crea ‘UsernamePasswordAuthenticationToken’ (no autenticado).

  4. Pasa al ‘AuthenticationManager’ (ProviderManager).

  5. ‘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()’.

  6. Si coincide, crea un nuevo ‘UsernamePasswordAuthenticationToken’ (autenticado) con el ‘UserDetails’.

  7. Guarda ese token en ‘SecurityContextHolder’.

  8. ‘SessionManagementFilter’ migra la sesión (prevención de fijación).

  9. Redirige a ‘/welcome’ (según tu config).

  10. 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.

Foto Cesar Fernandez

¿Lo rompiste? ¿Lo mejoraste?

Gracias por llegar hasta el final. Escribo estos posts para organizar mis propias ideas y, con suerte, para ahorrarle a alguien más el dolor de cabeza que yo ya pasé. Me encuentras en LinkedIn o puedes ver más de mi trabajo en GitHub.


Más Artículos