Spring Security Essentials Components

Spring Security: Essentials Components

Arquitectura General

En una aplicación Spring Boot tradicional sin seguridad, las peticiones HTTP llegan directamente a los controladores, se procesa la lógica de negocio y se devuelve una respuesta. Sin embargo, cuando integramos Spring Security, se introduce una capa intermedia de filtros de seguridad que interceptan y procesan cada petición antes de que llegue a nuestros controladores.

Flujo de una Petición en Spring Security

REQUEST → Filter Chain → Controllers → RESPONSE

image.png

Componentes Esenciales

1. Filter Chain (Cadena de Filtros)

La Filter Chain es una secuencia ordenada de filtros por los que debe pasar cada petición HTTP. Estos filtros se ejecutan antes de que la petición llegue al controlador.

Características importantes:

  • Incluso sin Spring Security, las aplicaciones Spring Boot tienen filtros, pero no son filtros relacionados con seguridad
  • Al configurar Spring Security, se añaden filtros específicos de seguridad a la cadena
  • Cada filtro tiene una responsabilidad específica y procesa la petición de manera secuencial
  • El número y tipo de filtros depende de la configuración de seguridad implementada

Tipos de filtros comunes en Spring Security:

  • UsernamePasswordAuthenticationFilter: Procesa intentos de autenticación basados en formularios
  • BasicAuthenticationFilter: Maneja autenticación HTTP Basic
  • JwtAuthenticationFilter: Procesa tokens JWT (cuando está configurado)
  • CsrfFilter: Protege contra ataques CSRF
  • CorsFilter: Gestiona políticas CORS
  • ExceptionTranslationFilter: Traduce excepciones de seguridad

2. Authentication Filter (Filtro de Autenticación)

El Authentication Filter es un filtro especializado que intercepta las peticiones de inicio de sesión (login).

Responsabilidades:

  • Detectar peticiones de autenticación (típicamente a endpoints como /login)
  • Extraer las credenciales del usuario (username, password u otros datos de autenticación)
  • Crear un objeto Authentication inicial con las credenciales proporcionadas
  • Delegar el proceso de validación al Authentication Manager

Flujo de trabajo:

  1. Usuario envía credenciales (username y password)
  2. El filtro intercepta la petición
  3. Extrae las credenciales de la petición
  4. Crea un objeto Authentication (inicialmente no autenticado)
  5. Pasa este objeto al Authentication Manager para su validación

3. Authentication Object (Objeto de Autenticación)

El Authentication Object es la representación central de la información de seguridad en Spring Security.

¿Qué contiene?

  • Principal: Identidad del usuario (generalmente el username)
  • Credentials: Credenciales (password, que se elimina después de la autenticación exitosa)
  • Authorities: Lista de permisos/roles del usuario
  • Authenticated: Bandera que indica si la autenticación ha sido validada
  • Details: Información adicional sobre la petición (IP, sesión, etc.)

Estados del objeto:

  • No autenticado: Contiene solo las credenciales proporcionadas por el usuario
  • Autenticado: Completamente poblado con roles, permisos y toda la información del usuario validado

4. Authentication Manager (Gestor de Autenticación)

El Authentication Manager es el coordinador central del proceso de autenticación.

Responsabilidades:

  • Recibir el objeto Authentication del filtro
  • Decidir qué estrategia de autenticación utilizar
  • Delegar la validación real a un Authentication Provider apropiado
  • Devolver el objeto Authentication autenticado (o lanzar una excepción si falla)

Implementación principal:

La implementación más común es ProviderManager, que mantiene una lista de Authentication Providers y los consulta secuencialmente hasta encontrar uno que pueda procesar la petición.

// Método principal
Authentication authenticate(Authentication authentication) throws AuthenticationException

5. Authentication Provider (Proveedor de Autenticación)

El Authentication Provider es el componente que realmente valida las credenciales del usuario.

Responsabilidad principal:

Verificar si las credenciales proporcionadas son correctas comparándolas con los datos almacenados en el sistema.

Para realizar esta validación, el Authentication Provider necesita:

  • UserDetailsService: Para cargar los datos del usuario desde la fuente de datos
  • PasswordEncoder: Para comparar contraseñas encriptadas

Tipos de Authentication Providers:

DaoAuthenticationProvider

El más utilizado. Valida usuarios contra una base de datos o cualquier fuente de datos accesible mediante DAO (Data Access Object).

Flujo:

  1. Recibe el objeto Authentication con las credenciales
  2. Usa UserDetailsService para cargar los detalles del usuario
  3. Usa PasswordEncoder para comparar la contraseña proporcionada con la almacenada
  4. Si coinciden, crea un objeto Authentication completo con roles y permisos
  5. Devuelve el objeto autenticado

InMemoryAuthenticationProvider

Utilizado para autenticación contra usuarios almacenados en memoria. Útil para:

  • Entornos de desarrollo
  • Prototipos
  • Aplicaciones con usuarios predefinidos

LdapAuthenticationProvider

Para integración con servidores LDAP (Lightweight Directory Access Protocol). Común en entornos empresariales donde la gestión de usuarios está centralizada.

JdbcAuthenticationProvider

Autenticación directa contra base de datos usando consultas JDBC personalizadas.

Métodos principales:

// Realiza la autenticación
Authentication authenticate(Authentication authentication) throws AuthenticationException

// Indica si este provider soporta un tipo específico de Authentication
boolean supports(Class<?> authentication)

6. UserDetailsService (Servicio de Detalles del Usuario)

El UserDetailsService es responsable de cargar la información del usuario desde la fuente de datos.

Responsabilidad:

Recuperar toda la información necesaria sobre un usuario específico, incluyendo:

  • Username
  • Password (encriptado)
  • Roles y authorities
  • Estado de la cuenta (bloqueada, expirada, habilitada, etc.)

Método principal:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException

Proceso:

  1. Recibe un username como parámetro
  2. Consulta la fuente de datos (base de datos, LDAP, API externa, etc.)
  3. Convierte la información del usuario en un objeto UserDetails
  4. Devuelve el objeto UserDetails al Authentication Provider

Implementaciones comunes:

  • JdbcUserDetailsManager: Carga usuarios desde base de datos usando JDBC
  • InMemoryUserDetailsManager: Gestiona usuarios en memoria
  • LdapUserDetailsService: Carga usuarios desde servidor LDAP
  • Implementación personalizada: Para lógicas de negocio específicas

Ejemplo de implementación personalizada:

@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"));

        return org.springframework.security.core.userdetails.User
            .withUsername(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRoles().toArray(new String[0]))
            .accountExpired(!user.isAccountNonExpired())
            .accountLocked(!user.isAccountNonLocked())
            .credentialsExpired(!user.isCredentialsNonExpired())
            .disabled(!user.isEnabled())
            .build();
    }
}

7. UserDetails (Objeto de Detalles del Usuario)

El UserDetails es una interfaz que representa al usuario autenticado.

Información que contiene:

  • Username: Identificador único del usuario
  • Password: Contraseña encriptada
  • Authorities: Colección de permisos/roles
  • Estado de la cuenta:
    • isAccountNonExpired(): ¿La cuenta no ha expirado?
    • isAccountNonLocked(): ¿La cuenta no está bloqueada?
    • isCredentialsNonExpired(): ¿Las credenciales no han expirado?
    • isEnabled(): ¿La cuenta está habilitada?

8. PasswordEncoder (Codificador de Contraseñas)

El PasswordEncoder es fundamental para la seguridad de las contraseñas.

Responsabilidades:

  • Encriptar contraseñas en texto plano antes de almacenarlas
  • Comparar contraseñas encriptadas durante la autenticación
  • Usar algoritmos seguros de hashing

Métodos principales:

// Encripta una contraseña en texto plano
String encode(CharSequence rawPassword)

// Compara una contraseña sin encriptar con una encriptada
boolean matches(CharSequence rawPassword, String encodedPassword)

Implementaciones comunes:

  • BCryptPasswordEncoder: Usa BCrypt (recomendado, adaptativo)
  • Argon2PasswordEncoder: Usa Argon2 (más moderno y seguro)
  • Pbkdf2PasswordEncoder: Usa PBKDF2
  • SCryptPasswordEncoder: Usa SCrypt
  • NoOpPasswordEncoder: Sin encriptación (DEPRECATED, solo para desarrollo)

Ejemplo de configuración:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12); // 12 rondas de hashing
}

9. Security Context (Contexto de Seguridad)

El Security Context es un contenedor que almacena la información de seguridad de la petición actual.

Características:

  • Almacena el objeto Authentication del usuario autenticado
  • Está disponible durante todo el ciclo de vida de la petición
  • Es thread-safe y se gestiona por petición HTTP
  • Permite acceder a la información del usuario en cualquier capa de la aplicación

Acceso al Security Context:

// Obtener el usuario autenticado
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

// Verificar si el usuario está autenticado
if (authentication != null && authentication.isAuthenticated()) {
    // Usuario autenticado
}

Estrategias de almacenamiento:

  • MODE_THREADLOCAL: Una instancia por thread (por defecto)
  • MODE_INHERITABLETHREADLOCAL: Se hereda a threads hijos
  • MODE_GLOBAL: Una única instancia global (raramente usado)

Flujo Completo de Autenticación: Paso a Paso

Veamos el proceso completo de autenticación cuando un usuario intenta iniciar sesión:

Paso 1: Envío de Credenciales

El usuario envía una petición de login con su username y password (por ejemplo, a través de un formulario web o petición POST).

POST /login
Content-Type: application/x-www-form-urlencoded

username=juan&password=miPassword123

Paso 2: Interceptación por el Authentication Filter

  • La petición pasa por la cadena de filtros
  • El Authentication Filter (como UsernamePasswordAuthenticationFilter) intercepta la petición
  • Extrae las credenciales: username y password
  • Crea un objeto Authentication inicial (aún no autenticado)
UsernamePasswordAuthenticationToken authRequest =
    new UsernamePasswordAuthenticationToken(username, password);

Paso 3: Delegación al Authentication Manager

El filtro pasa el objeto Authentication al Authentication Manager.

Authentication authResult = authenticationManager.authenticate(authRequest);

Paso 4: Selección del Authentication Provider

El Authentication Manager (típicamente ProviderManager):

  • Revisa su lista de Authentication Providers
  • Selecciona el apropiado (generalmente DaoAuthenticationProvider)
  • Delega la validación a ese provider

Paso 5: Carga de Datos del Usuario

El Authentication Provider llama al UserDetailsService:

UserDetails userDetails = userDetailsService.loadUserByUsername(username);

El UserDetailsService:

  • Consulta la base de datos (o fuente de datos correspondiente)
  • Busca el usuario por username
  • Carga toda la información: password encriptado, roles, estado de cuenta
  • Devuelve un objeto UserDetails

Paso 6: Validación de la Contraseña

El Authentication Provider usa el PasswordEncoder para comparar contraseñas:

boolean passwordMatch = passwordEncoder.matches(
    rawPassword,  // Contraseña proporcionada por el usuario
    userDetails.getPassword()  // Contraseña encriptada almacenada
);

Paso 7: Validaciones Adicionales

El provider verifica también:

  • ¿La cuenta está habilitada? (isEnabled())
  • ¿La cuenta no está bloqueada? (isAccountNonLocked())
  • ¿Las credenciales no han expirado? (isCredentialsNonExpired())
  • ¿La cuenta no ha expirado? (isAccountNonExpired())

Paso 8: Creación del Authentication Object Completo

Si todas las validaciones pasan, el Authentication Provider:

  • Crea un nuevo objeto Authentication completamente poblado
  • Incluye el username, authorities (roles/permisos)
  • Marca el objeto como autenticado
  • Importante: Por seguridad, se elimina la contraseña del objeto
Authentication authenticatedToken = new UsernamePasswordAuthenticationToken(
    userDetails,
    null,  // La contraseña se elimina por seguridad
    userDetails.getAuthorities()
);

Paso 9: Retorno al Authentication Manager y Filter

  • El objeto autenticado vuelve al Authentication Manager
  • Este lo devuelve al Authentication Filter

Paso 10: Establecimiento del Security Context

El Authentication Filter:

  • Almacena el objeto Authentication en el Security Context
  • Este contexto estará disponible durante toda la petición
SecurityContextHolder.getContext().setAuthentication(authenticatedToken);

Paso 11: Continuación de la Petición

  • La petición continúa por la cadena de filtros
  • Eventualmente llega al Controller
  • El código de la aplicación se ejecuta con el usuario ya autenticado

Paso 12: Autorización Durante la Petición

Durante toda la petición:

  • Los filtros de autorización consultan el Security Context
  • Verifican si el usuario tiene los roles/permisos necesarios
  • Permiten o deniegan el acceso a recursos según la configuración
@PreAuthorize("hasRole('ADMIN')")
public void adminOperation() {
    // Solo usuarios con rol ADMIN pueden ejecutar esto
}

Autorización: Roles y Permisos

Una vez autenticado el usuario, Spring Security gestiona la autorización, determinando qué puede y qué no puede hacer el usuario en el sistema.

Roles vs Authorities

  • Authority: Permiso individual (ej: “READ_USERS”, “DELETE_POST”)
  • Role: Conjunto de authorities (ej: “ROLE_ADMIN”, “ROLE_USER”)

Convención: Los roles en Spring Security se prefijan con “ROLE_”

Configuración de Autorización

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .requestMatchers("/api/**").hasAuthority("API_ACCESS")
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());

        return http.build();
    }
}

Autorización a Nivel de Método

@Service
public class UserService {

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long userId) {
        // Solo ADMIN puede eliminar usuarios
    }

    @PreAuthorize("#username == authentication.principal.username or hasRole('ADMIN')")
    public User getUserDetails(String username) {
        // Un usuario puede ver sus propios detalles, o un ADMIN puede ver cualquiera
    }

    @PostAuthorize("returnObject.owner == authentication.principal.username")
    public Document getDocument(Long id) {
        // Solo el propietario del documento puede acceder
    }
}

Ejemplo de Configuración Completa

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .permitAll()
            );

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

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

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
}

Diagrama de Flujo Resumido

1. REQUEST → Filter Chain

2. Authentication Filter

   Extrae credenciales

   Crea Authentication Object (no autenticado)

3. Authentication Manager

   Delega a Authentication Provider

4. Authentication Provider

   ├─→ UserDetailsService.loadUserByUsername()
   │   ↓
   │   Database → UserDetails

   └─→ PasswordEncoder.matches()

       Valida contraseña

5. Si es válido:

   Crea Authentication Object (autenticado con roles)

   Retorna a Authentication Manager

   Retorna a Authentication Filter

6. Security Context se establece

   SecurityContextHolder.setContext()

7. Request continúa → Controller

   Durante la petición: autorización basada en roles

8. RESPONSE

Mejores Prácticas

1. Seguridad de Contraseñas

  • Siempre usa un PasswordEncoder fuerte (BCrypt, Argon2)
  • Nunca almacenes contraseñas en texto plano
  • Configura suficientes rondas de hashing (BCrypt: 10-12)

2. Gestión de Sesiones

http.sessionManagement(session -> session
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Para APIs REST
    .maximumSessions(1) // Limitar sesiones concurrentes
    .expiredUrl("/login?expired")
);

3. Protección CSRF

  • Habilita CSRF para aplicaciones web con formularios
  • Desactívalo solo para APIs stateless con autenticación por token

4. CORS

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("https://miapp.com"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
    configuration.setAllowedHeaders(Arrays.asList("*"));
    configuration.setAllowCredentials(true);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

5. Logging y Auditoría

@Component
public class AuthenticationEventListener {

    @EventListener
    public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
        log.info("Usuario autenticado: {}", event.getAuthentication().getName());
    }

    @EventListener
    public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) {
        log.warn("Intento fallido de autenticación: {}", event.getAuthentication().getName());
    }
}

Recursos adicionales: