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.
Al terminar sabrás
- Seguir el viaje de una petición desde Tomcat hasta tu controlador.
- Explicar qué hacen
DelegatingFilterProxy,FilterChainProxyySecurityFilterChain. - 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.
El viaje de una petición
Sección titulada «El viaje de una petición»DelegatingFilterProxypuente hacia los beansFilterChainProxyelige UNA cadena, limpia el contextocadena 1 · /api/**elegida
SecurityContextHolderFiltercontextoHeaderWriterFilterprotección contra exploitsCorsFilterprotección contra exploitsCsrfFilterprotección contra exploitsLogoutFilterautenticaciónBearerTokenAuthenticationFilterautenticaciónBasicAuthenticationFilterautenticaciónAnonymousAuthenticationFilterautenticaciónExceptionTranslationFiltererroresAuthorizationFilterautorización
cadena 2 · todo lo demás
…CsrfFilterUsernamePassword……
DispatcherServlet→ @RestController · tu controladorLas tres piezas, de fuera hacia dentro
Sección titulada «Las tres piezas, de fuera hacia dentro»DelegatingFilterProxy. Tomcat solo sabe registrar objetosFilteral 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 llamadospringSecurityFilterChainy le delega todo. Spring Boot lo registra por ti.FilterChainProxy. Ese bean. Es el punto de entrada de toda la seguridad: elige qué cadena aplicar, la recorre, aplica elHttpFirewall(que rechaza URLs maliciosas como path traversal) y limpia elSecurityContextal 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.SecurityFilterChain. Una lista de filtros asociada a unRequestMatcher. Puedes tener varias (por ejemplo, una para/api/**y otra para la web).FilterChainProxylas prueba en orden y solo ejecuta la primera que coincide; las demás ni se miran.
Por qué el orden importa
Sección titulada «Por qué el orden importa»Cada filtro supone que los anteriores ya hicieron su trabajo. Agrupados por función:
| Orden | Filtros | Qué hacen |
|---|---|---|
| 1 | SecurityContextHolderFilter | Carga el contexto de seguridad (de la sesión, si existe). Todos los demás lo necesitan. |
| 2 | HeaderWriterFilter, CorsFilter, CsrfFilter | Protección contra exploits. No tiene sentido verificar una contraseña si la petición es un ataque CSRF. |
| 3 | LogoutFilter, UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter, BearerTokenAuthenticationFilter | Intentan identificarte. Si no traes credenciales, no fallan: dejan pasar. |
| 4 | AnonymousAuthenticationFilter | Si nadie te identificó, pone un usuario anónimo. El contexto nunca queda vacío. |
| 5 | ExceptionTranslationFilter | Envuelve a los siguientes en un try/catch y convierte excepciones de seguridad en 401/403. |
| 6 | AuthorizationFilter | Al 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.
Ver la cadena real
Sección titulada «Ver la cadena real»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, AuthorizationFilterY 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.
Escribir un filtro propio
Sección titulada «Escribir un filtro propio»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:
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 } }}@BeanSecurityFilterChain 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 de | Lo que ya ocurrió |
|---|---|---|
| Protección contra exploits | SecurityContextHolderFilter | Contexto cargado |
| Autenticación | LogoutFilter | Contexto + protecciones |
| Autorización | AnonymousAuthenticationFilter | Contexto + 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.
- 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. - Busca la línea de
DefaultSecurityFilterChainy cuenta los filtros. - Crea un
@RestControllerconGET /api/helloque devuelva"hola". - Ejecuta estas peticiones y lee el log de cada una:
curl -i http://localhost:8080/api/hellocurl -i -u user:<contraseña> http://localhost:8080/api/hellocurl -i -X POST -u user:<contraseña> http://localhost:8080/api/hello- Añade el
RequestIdFilterde arriba y comprueba siX-Request-Idaparece en la respuesta 401.
Preguntas:
- En la primera petición, ¿qué filtro decidió el 401: uno de autenticación o
AuthorizationFilter? - El
POSTcon credenciales correctas da 403, no 401. ¿Qué filtro lo rechazó y por qué tus credenciales no lo evitaron? - ¿Por qué
X-Request-Idaparece 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,BasicAuthenticationFilterno hace nada yAnonymousAuthenticationFilterpone un usuario anónimo. Al llegar al final, la regla por defecto (anyRequest().authenticated()) lo deniega,AuthorizationFilterlanzaAccessDeniedException, yExceptionTranslationFilterla convierte en 401 porque el usuario es anónimo. En el log verás algo comoSending 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, yCsrfFilterse ejecuta antes de los filtros de autenticación. Comocurlno 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 adelanteExceptionTranslationFilterresponde 401, la cabecera ya estaba en la respuesta. Así puedes correlacionar incluso los rechazos con su línea de log.