CodeGym /Cursos /Spring Security /Etapas de la petición en la security chain

Etapas de la petición en la security chain

Spring Security
Nivel 3, Lección 2
Disponible

1. Etapas en vez de lista de filtros

Una lista de filtros de por sí apenas ayuda. Cuando por primera vez abres los logs de Spring Security o ves la enumeración de filtros, es fácil caer en la trampa: "Vaya, aquí hay 15-20 clases, ahora me aprendo sus nombres — y entenderé todo". Eso es más o menos como intentar convertirse en cocinero aprendiendo los nombres de todas las cacerolas de la cocina. En esta lección hacemos algo más útil: convertimos la "multitud de filtros" en etapas comprensibles del procesamiento de la petición, donde cada etapa responde a una pregunta simple y prepara los datos para la siguiente.

Es importante entender el objetivo: no necesitamos un "catálogo de filtros". Necesitamos un esquema con el que puedas explicarte a ti mismo y a un colega: por qué la petición llegó al controlador, por qué no llegó, por qué apareció un redirect, por qué "de repente 403" y por qué el mismo endpoint se comporta de forma distinta en el navegador y en curl.

Modelo: la petición como pipeline de "puertas"

Imagina un aeropuerto. Entrar al edificio no significa que ya estés en el avión. Después vienen etapas: el arco detector de metales, la comprobación de documentos, el registro del equipaje de mano, y solo entonces — la salida a las puertas de embarque. Con la seguridad de las peticiones la historia es parecida: la petición no se "teletransporta" al controlador. Pasa por un pipeline donde en cada paso o bien obtiene el contexto necesario, o bien se detiene con una respuesta comprensible.

Técnicamente ese pipeline está implementado dentro de FilterChainProxy, que delega el procesamiento a la SecurityFilterChain concreta. Precisamente esa infraestructura crea la sensación de "magia", si no se ven las etapas. Pero en cuanto las etapas se organizan en la cabeza, la magia se convierte en mecánica de ingeniería normal: "en la etapa X preparamos el contexto, en la etapa Y intentamos entender al usuario, en la etapa Z tomamos la decisión sobre el acceso".

Para mantener esto en la cabeza, usaremos el esquema didáctico de cinco etapas — no es el "nombre oficial de las fases", sino un corte didáctico conveniente:

import java.util.List;

public class SecurityStagesDemo {

    public static void main(String[] args) {
        // Modelo didáctico: agrupamos los filtros no por clases, sino por responsabilidades (etapas).
        List<String> stages = List.of(
                // 1) Preparar el lugar donde almacenamos el estado de security de la petición
                "prepare SecurityContext",
                // 2) Intentar autenticar, si la petición contiene "documentos"
                "try to authenticate (if request contains credentials)",
                // 3) Si el usuario aún no aparece — hacer el estado explícito a través de anonymous
                "fallback to anonymous (if still no user)",
                // 4) Convertir las excepciones de security en una respuesta HTTP comprensible (no en un stacktrace)
                "translate security exceptions into HTTP response",
                // 5) Tomar la decisión sobre el acceso antes del controlador: dejar pasar o no
                "authorize request (allow / deny) before controller"
        );

        // Comprobamos que realmente son 5 etapas (puramente para autocomprobar el esquema).
        System.out.println(stages.size()); // 5
    }
}

Este esquema simplifica intencionadamente la realidad. Es una agrupación didáctica de responsabilidades, no un inventario literal de filtros ni un orden rígido igual para cualquier petición. En la cadena real un filtro puede envolver a los siguientes, parte de las etapas puede verse de otra forma, y el conjunto de filtros de autenticación depende de cómo exactamente presenta el cliente. Pero por responsabilidades el cuadro siempre se organiza aproximadamente así.

2. Etapa 1 — preparación del SecurityContext: "no trabajamos en el vacío"

Cualquier sistema serio empieza por poner el "lugar de trabajo" en orden. En Spring Security ese "lugar de trabajo" es el SecurityContext: el contenedor donde vive el estado de security actual de la petición. En la primera etapa la cadena garantiza que el contexto existe, aunque esté vacío, y que los filtros posteriores no necesitan adivinar "¿dónde está el usuario aquí?", sino mirar en un solo lugar.

A nivel de infraestructura es importante recordar que FilterChainProxy es el punto central de servlet-security de Spring Security. En la documentación se subraya que es el punto de partida y que hace cosas obligatorias como limpiar el SecurityContext, para que no haya filtraciones entre peticiones. Eso es como "limpiar la mesa después de cada pasajero": parece aburrido, pero de lo contrario se vuelve muy rápidamente aterrador.

En las versiones modernas de Spring Security hay un filtro que lee el SecurityContext del almacén y rellena el SecurityContextHolder, y luego toda la petición trabaja a través de ese holder. Históricamente era el SecurityContextPersistenceFilter, y más tarde la documentación describe el movimiento hacia el SecurityContextHolderFilter, que lee y rellena el contexto, y el guardado lo hace de forma más explícita. Por ahora no profundizamos en dónde exactamente se almacena el contexto entre peticiones. Para hoy lo importante es el hecho en sí: sin el contexto las etapas posteriores no pueden tomar decisiones coherentes.

Una pequeña sensación de esta etapa se puede obtener incluso sin entender los detalles del almacenamiento:

import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

public class ContextPeek {

    public static void peek() {
        // SecurityContext habitualmente se almacena en ThreadLocal a través de SecurityContextHolder:
        // eso significa que el "contexto actual" está vinculado al hilo actual de procesamiento de la petición.
        SecurityContext context = SecurityContextHolder.getContext();

        // En la aplicación real aquí habitualmente habrá un Authentication dentro del contexto (o vacío).
        System.out.println(context); // (mostrará el objeto del contexto, el formato depende de la implementación)
    }
}

Idea clave: no creas el SecurityContext manualmente en el controlador. Lo prepara la cadena de security, para que luego cualquier código en el marco de la petición pueda preguntar: "¿y quién hace la petición ahora?" — y recibir la respuesta de un solo lugar.

3. Etapa 2 — intentar entender al usuario: "los datos de entrada"

Cuando el contexto está preparado, tiene lógica plantear la siguiente pregunta: "¿y esta petición es de alguien concreto o simplemente un transeúnte?". En esta etapa los filtros miran la petición y comprueban si hay en ella signos de que el cliente intenta presentarse. Pueden ser distintos tipos de presentación de documentos: la cabecera Authorization, una cookie, los parámetros del formulario de login, etc. Por ahora lo importante es el propio momento de aparición de los datos de entrada, sin analizar la comprobación interna del login y la contraseña.

Un matiz importante para el novato: los filtros en esta etapa no están obligados a autenticar al usuario. Pueden simplemente entender que no hay datos, y entonces no tocar el contexto o dejarlo vacío hasta la siguiente etapa. Y si hay datos, lanzan el mecanismo de autenticación, que ya internamente decidirá: "creo / no creo". Por ahora lo importante es ver el propio lugar en la cadena donde eso ocurre.

A continuación, por simpleza, solo ejemplos con cabeceras. Eso no significa que Spring Security siempre busque exactamente Authorization: las credenciales pueden llegar también mediante form login, cookies y otros mecanismos.

El pseudocódigo didáctico que ilustra la idea puede tener este aspecto:

public class CredentialsProbe {

    public static boolean hasCredentials(String authorizationHeader) {
        // Si no hay cabecera — el cliente no intentó presentar "documentos".
        if (authorizationHeader == null) {
            return false;
        }

        // Los esquemas más populares: Basic (login/contraseña) y Bearer (token).
        // En esta etapa nos interesa solo "¿hay un intento de presentarse?".
        return authorizationHeader.startsWith("Basic ")
                || authorizationHeader.startsWith("Bearer ");
    }
}

Sí, la palabra Bearer es demasiado pronto para amarla, pero como ejemplo de "en la petición hay un intento de presentarse" encaja. El significado de la etapa es simple: la cadena no adivina al usuario por telepatía, o bien encuentra los datos de entrada, o bien reconoce honestamente: "aquí no hay nada que autenticar".

4. Etapa 3 — anonymous: "que sea un estado explícito, no 'nada'"

Después del intento de autenticación son posibles dos escenarios: o en el contexto aparece un usuario real, o sigue sin haber nadie. Y aquí Spring Security tiene una filosofía importante: "nadie" es un mal estado, porque difumina la lógica. Es mucho más conveniente tener un principal anonymous explícito, para que luego todas las comprobaciones funcionen de forma uniforme.

Precisamente por eso en la cadena está el AnonymousAuthenticationFilter. El nombre ya lo dice todo: convierte el vacío en un estado técnicamente descrito. La lista de filtros que aparece en la documentación y en JavaDoc directamente incluye el AnonymousAuthenticationFilter entre los participantes típicos de la cadena.

¿Por qué eso no es burocracia innecesaria? Porque más adelante en la etapa de autorización no hay que escribir ramas distintas "si Authentication == null…". En cambio se puede pensar así: "hay un usuario autenticado o un usuario anónimo". Y eso influye elegantemente en la semántica del tipo "dejar pasar a todos" o "dejar pasar solo a los que entraron".

Aquí lo importante es el propio papel de la etapa: el vacío se convierte en una entidad explícita; los detalles de Authentication y Principal por ahora no son necesarios.

5. Etapa 4 — traducción de excepciones a HTTP: ExceptionTranslationFilter

Si las etapas 1-3 responden por "preparar el estado de la petición", la etapa 4 es por "si algo salió mal, responder al cliente de forma normal". La cadena de security debe saber detener la petición de forma predecible: no caer con un stacktrace como respuesta y no dejar que la excepción vaya a parar a un manejador aleatorio. Para eso en la cadena hay un filtro que traduce los errores de security en respuestas HTTP.

En Spring Security eso lo hace el ExceptionTranslationFilter. En la documentación se describe directamente como el mecanismo que traduce AuthenticationException y AccessDeniedException en respuestas HTTP, usando AuthenticationEntryPoint para iniciar la autenticación y AccessDeniedHandler para manejar el rechazo de acceso.

Fíjate en la mecánica importante: este filtro invoca chain.doFilter(...), es decir deja pasar la petición más adelante, pero si más adelante alguien lanza una excepción de security, él la intercepta y la convierte en respuesta. En la documentación incluso se da pseudocódigo del tipo try doFilter, catch exceptions, y luego — o lanzar la autenticación, o rechazar el acceso.

Aquí una mini-versión didáctica de esa idea:

public class ExceptionTranslationLike {

    public static String handle(boolean authenticated, boolean accessDenied) {
        try {
            // Simulamos la situación "acceso denegado" en algún punto más profundo de la cadena.
            // En realidad serían AccessDeniedException / AuthenticationException.
            if (accessDenied) {
                throw new RuntimeException("AccessDenied"); // simplificamos para el ejemplo
            }

            // Si no hay errores — la cadena "deja pasar la petición más adelante".
            return "OK";
        } catch (RuntimeException ex) {
            // Bifurcación importante: si el usuario no está autenticado,
            // la respuesta habitualmente será sobre "iniciar la autenticación".
            if (!authenticated) {
                return "Start authentication";
            }

            // Si el usuario está autenticado pero no tiene permisos — "acceso denegado".
            return "Access denied";
        }
    }
}

Esto no es código de Spring Security, sino simplemente la ilustración del sentido: en un lugar la cadena decide qué decirle al cliente cuando el acceso es imposible.

6. Etapa 5 — autorización de la petición: "¿dejar pasar o no?"

Cuando ya tenemos un estado de security comprensible — el contexto y o bien el usuario o bien anonymous —, la cadena llega a la pregunta práctica principal: "¿puede esta petición ir más adelante al DispatcherServlet, los controladores y la lógica de negocio?" Eso es la etapa de autorización: la comprobación del acceso.

En las cadenas modernas el papel de "comprobar el acceso" lo realizan filtros a nivel de autorización, y en la lista de filtros típicos encontrarás AuthorizationFilter, así como elementos históricos como FilterSecurityInterceptor. Ahora no nos importa cómo exactamente describirás las reglas, lo importante es entender el momento: la decisión sobre el acceso puede tomarse antes incluso del controlador.

Y aquí se vuelve comprensible la observación del día anterior: tras conectar el security-starter casi todo se cerró. Eso ocurrió no porque los controladores de repente se volvieran malvados y empezaran a lanzar 403, sino porque en la etapa de autorización la cadena de security dijo: "stop, no se puede seguir" — y la petición terminó antes de MVC.

A nivel de modelo simple en la cabeza esto se puede representar así:

public class AuthorizationDecisionDemo {

    public static String decide(String path, boolean authenticated) {
        // Condicionalmente asumimos que todo bajo /api/public/ es público.
        // (Cómo exactamente se configura eso en Spring Security — tema aparte.)
        boolean isPublic = path.startsWith("/api/public/");

        // Lo público — lo dejamos pasar sin condiciones.
        if (isPublic) return "ALLOW";

        // Lo no público — exigimos que el usuario sea "no anonymous".
        if (!authenticated) return "DENY (needs authentication)";

        // Si el usuario está autenticado — permitimos.
        return "ALLOW";
    }
}

No discutimos cómo Spring Security sabe que /api/public/** debe ser public — eso será un tema aparte. Aquí lo importante es asentar otra cosa: la autorización es una etapa aparte que usa los resultados de las etapas tempranas, es decir el contexto y el usuario/anonymous.

7. El orden de las etapas es crítico: romper todo con un paso

En teoría se pueden inventar docenas de etapas y aún más bonitas flechas. Pero el valor de ingeniería aparece en el momento en que entiendes: el orden de las etapas no es estética, sino dependencia de datos. Una etapa tardía no puede funcionar si la temprana no preparó los datos de entrada. No se puede comprobar honestamente el acceso si aún no se ha llevado el "usuario actual" a un estado comprensible.

Por ejemplo, si la autorización empieza antes de que la cadena haya decidido "authenticated vs anonymous", la comprobación verá constantemente null o empezará a llenarse de ramas y convertirse en un popurrí. Por eso las ideas de "pongamos los filtros en otro orden para que funcione" suelen terminar en que funciona… pero solo en un navegador y solo los martes.

Ilustración didáctica de por qué anonymous antes de la comprobación de acceso es más lógico que al revés:

import java.util.List;

public class OrderingDemo {

    public static void main(String[] args) {
        // Lógica incorrecta: primero comprobamos el acceso, luego "ponemos orden" con anonymous.
        List<String> wrongOrder = List.of("authorize", "set anonymous");

        // Lógica correcta: primero llevamos el estado del usuario a una forma explícita,
        // y solo después tomamos la decisión sobre el acceso.
        List<String> goodOrder = List.of("set anonymous", "authorize");

        // Imprimimos simplemente para fijar visualmente la idea del orden.
        System.out.println(wrongOrder); // [authorize, set anonymous]
        System.out.println(goodOrder);  // [set anonymous, authorize]
    }
}

Si se mantienen precisamente las dependencias de las etapas, la lista real de filtros deja de asustar. Ya no parece aleatoria y empieza a leerse como "muchos pequeños pasos, cada uno hace algo comprensible".

8. Errores típicos al intentar entender las etapas de la security chain

Error nº 1: intentar aprender Spring Security como una lista de clases.
Ese enfoque a menudo lleva al efecto "lo aprendí todo y sigo sin entender nada". Los nombres de los filtros se parecen unos a otros, y la lógica real se pierde. Es mucho más útil asentar las cinco etapas y solo después vincularles clases concretas como AnonymousAuthenticationFilter o ExceptionTranslationFilter.

Error nº 2: pensar que la decisión de security se toma en el controlador.
Tras un par de situaciones del tipo "¿por qué no se invocó el método?", el novato empieza a sospechar que Spring se rompió. En realidad la petición pudo detenerse en la etapa de autorización o en la etapa de manejo de excepciones de security — y al DispatcherServlet simplemente no llegó. Eso es absolutamente normal y correcto desde el punto de vista del diseño.

Error nº 3: confundir "no hay usuario" y "usuario anonymous".
Cuando parece que anonymous = null, empiezan comprobaciones extrañas y el caos. En Spring Security anonymous es un modelo intencional del estado, para que las etapas posteriores funcionen de forma uniforme.

Error nº 4: no entender el papel del ExceptionTranslationFilter y esperar que cualquier fallo en security será simplemente una excepción.
Si no se conoce esa etapa, es fácil sorprenderse: "¿por qué no veo el stacktrace?" o "¿por qué me redirige?". Traducir las excepciones en respuestas HTTP es una responsabilidad de la capa de security. Intercepta las excepciones necesarias y las convierte en una reacción para el cliente.

Error nº 5: intentar "arreglar el 403" cambiando el orden de los filtros a ciegas.
Si se cambia el orden o el conjunto de filtros sin entender las dependencias de las etapas, es fácil obtener un sistema que parece funcionar, pero falla en otros tipos de peticiones: por ejemplo, en curl todo va bien, pero en el navegador hay redirects, o al revés. El orden de las etapas es dependencia de datos, no una preferencia estética.

1
Tarea
Spring Security, nivel 3, lección 2
Bloqueada
Modelo de etapas mediante dos filtros y atributos de solicitud
Modelo de etapas mediante dos filtros y atributos de solicitud
1
Tarea
Spring Security, nivel 3, lección 2
Bloqueada
Simulador de consola de etapas de security chain
Simulador de consola de etapas de security chain
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION