Ir al contenido

Spring SecurityLección 01

La arquitectura de filtros

Spring Security no es magia, es una lista ordenada de filtros que se ejecutan antes que tu controlador. Si entiendes esa lista, puedes explicar cualquier 401, 403 o redirección.

Intermedio15 min de lectura

Al terminar sabrás

  • Seguir el viaje de una petición desde Tomcat hasta tu controlador.
  • Explicar qué hacen DelegatingFilterProxy, FilterChainProxy y SecurityFilterChain.
  • Razonar por qué el orden de los filtros importa y quién rechaza realmente una petición.
  • Escribir y colocar un filtro propio sin que se ejecute dos veces.

Toda la seguridad de Spring para aplicaciones Servlet (Spring MVC) vive en filtros: objetos que interceptan la petición antes de que llegue al DispatcherServlet y, por tanto, antes de tu @RestController. No hay anotaciones mágicas ni proxies ocultos en esta capa: hay una lista, y la petición la recorre en orden.

Contenedor de Servlets (Tomcat)
DelegatingFilterProxypuente hacia los beans
FilterChainProxyelige UNA cadena, limpia el contexto

cadena 1 · /api/**elegida

  1. SecurityContextHolderFiltercontexto
  2. HeaderWriterFilterprotección contra exploits
  3. CorsFilterprotección contra exploits
  4. CsrfFilterprotección contra exploits
  5. LogoutFilterautenticación
  6. BearerTokenAuthenticationFilterautenticación
  7. BasicAuthenticationFilterautenticación
  8. AnonymousAuthenticationFilterautenticación
  9. ExceptionTranslationFiltererrores
  10. AuthorizationFilterautorización

cadena 2 · todo lo demás

  1. …
  2. CsrfFilter
  3. UsernamePassword…
  4. …
DispatcherServlet→ @RestController · tu controlador
Tomcat → DelegatingFilterProxy → FilterChainProxy → la primera SecurityFilterChain que coincide → DispatcherServlet.
  1. DelegatingFilterProxy. Tomcat solo sabe registrar objetos Filter al arrancar; no conoce los beans de Spring, que se crean después. Este filtro es el puente: Tomcat lo registra y él busca, de forma perezosa, un bean llamado springSecurityFilterChain y le delega todo. Spring Boot lo registra por ti.
  2. FilterChainProxy. Ese bean. Es el punto de entrada de toda la seguridad: elige qué cadena aplicar, la recorre, aplica el HttpFirewall (que rechaza URLs maliciosas como path traversal) y limpia el SecurityContext al terminar para que un hilo reutilizado no herede la identidad de otro usuario. Es el mejor sitio para un breakpoint cuando no entiendes qué pasa.
  3. SecurityFilterChain. Una lista de filtros asociada a un RequestMatcher. Puedes tener varias (por ejemplo, una para /api/** y otra para la web). FilterChainProxy las prueba en orden y solo ejecuta la primera que coincide; las demás ni se miran.

Cada filtro supone que los anteriores ya hicieron su trabajo. Agrupados por función:

OrdenFiltrosQué hacen
1SecurityContextHolderFilterCarga el contexto de seguridad (de la sesión, si existe). Todos los demás lo necesitan.
2HeaderWriterFilter, CorsFilter, CsrfFilterProtección contra exploits. No tiene sentido verificar una contraseña si la petición es un ataque CSRF.
3LogoutFilter, UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter, BearerTokenAuthenticationFilterIntentan identificarte. Si no traes credenciales, no fallan: dejan pasar.
4AnonymousAuthenticationFilterSi nadie te identificó, pone un usuario anónimo. El contexto nunca queda vacío.
5ExceptionTranslationFilterEnvuelve a los siguientes en un try/catch y convierte excepciones de seguridad en 401/403.
6AuthorizationFilterAl final: aplica las reglas de authorizeHttpRequests. Si dice que no, lanza AccessDeniedException.

La excepción son las credenciales inválidas: si envías una contraseña incorrecta o un token mal firmado, el filtro de autenticación sí corta la petición con un 401 en ese momento.

Con el log de seguridad en DEBUG o TRACE, Spring imprime al arrancar cada cadena con sus filtros. Con la configuración por defecto de Spring Boot verás algo así (la lista exacta depende de lo que tengas activado):

DefaultSecurityFilterChain: Will secure any request with filters:
DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter,
HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter,
DefaultResourcesFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter,
BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter,
AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter

Y en TRACE, cada petición muestra el recorrido filtro a filtro (Invoking CsrfFilter (5/16)…) y la decisión final. Ese log es tu principal herramienta de diagnóstico: Spring nunca pone la razón de un rechazo en la respuesta.

Un filtro custom se escribe extendiendo OncePerRequestFilter (garantiza una sola ejecución por petición, incluso con forwards internos) y se coloca en la cadena relativo a un filtro existente:

RequestIdFilter.java
public class RequestIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String id = UUID.randomUUID().toString();
response.setHeader("X-Request-Id", id); // antes de continuar: llega incluso a un 401
MDC.put("requestId", id); // aparece en todas las líneas de log
try {
chain.doFilter(request, response); // deja continuar la petición
} finally {
MDC.remove("requestId"); // a la vuelta: limpia
}
}
}
SecurityConfig.java
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.addFilterBefore(new RequestIdFilter(), SecurityContextHolderFilter.class)
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}

¿Dónde colocarlo? La guía de la documentación oficial:

Si tu filtro es de…Colócalo después deLo que ya ocurrió
Protección contra exploitsSecurityContextHolderFilterContexto cargado
AutenticaciónLogoutFilterContexto + protecciones
AutorizaciónAnonymousAuthenticationFilterContexto + protecciones + usuario identificado (o anónimo)

Mini lab · 20 min

Ver la cadena por dentro

Objetivo: Observar los filtros reales y descubrir quién decide cada respuesta, sin escribir configuración de seguridad.

  1. Arranca el proyecto base sin ninguna clase de configuración de seguridad, con logging.level.org.springframework.security=TRACE. Copia la contraseña generada del log.
  2. Busca la línea de DefaultSecurityFilterChain y cuenta los filtros.
  3. Crea un @RestController con GET /api/hello que devuelva "hola".
  4. Ejecuta estas peticiones y lee el log de cada una:
Ventana de terminal
curl -i http://localhost:8080/api/hello
curl -i -u user:<contraseña> http://localhost:8080/api/hello
curl -i -X POST -u user:<contraseña> http://localhost:8080/api/hello
  1. Añade el RequestIdFilter de arriba y comprueba si X-Request-Id aparece en la respuesta 401.

Preguntas:

  • En la primera petición, ¿qué filtro decidió el 401: uno de autenticación o AuthorizationFilter?
  • El POST con credenciales correctas da 403, no 401. ¿Qué filtro lo rechazó y por qué tus credenciales no lo evitaron?
  • ¿Por qué X-Request-Id aparece en el 401 si la petición fue rechazada?
Pista

Para el POST, fíjate en qué filtro va antes que BasicAuthenticationFilter en la lista y qué protege.

Mostrar respuestaOcultar respuesta
  • AuthorizationFilter. Sin credenciales, BasicAuthenticationFilter no hace nada y AnonymousAuthenticationFilter pone un usuario anónimo. Al llegar al final, la regla por defecto (anyRequest().authenticated()) lo deniega, AuthorizationFilter lanza AccessDeniedException, y ExceptionTranslationFilter la convierte en 401 porque el usuario es anónimo. En el log verás algo como Sending AnonymousAuthenticationToken … to authentication entry point since access is denied.
  • Lo rechazó CsrfFilter. La configuración por defecto protege contra CSRF los métodos que cambian estado, y CsrfFilter se ejecuta antes de los filtros de autenticación. Como curl no envía token CSRF, la petición se corta con 403 antes de mirar siquiera tus credenciales. (En la lección de CSRF verás cuándo es correcto desactivarlo).
  • Porque el filtro escribe la cabecera antes de llamar a chain.doFilter, y está colocado al principio de la cadena. Cuando más adelante ExceptionTranslationFilter responde 401, la cabecera ya estaba en la respuesta. Así puedes correlacionar incluso los rechazos con su línea de log.