Spring SecurityLección 05
Sesiones y persistencia del contexto
El SecurityContext vive lo que dura una petición. Qué lo guarda entre peticiones, cómo elegir entre stateful y stateless y cómo defender la sesión.
Al terminar sabrás
- Explicar el papel del
SecurityContextRepositoryen apps stateful y stateless. - Guardar el contexto a mano cuando autenticas en tu propio controlador.
- Elegir la política de creación de sesión correcta.
- Defender la sesión frente a fijación, robo de cookie y sesiones concurrentes.
El SecurityContextHolder dura una petición: FilterChainProxy lo limpia al terminar. Para que el usuario siga autenticado en la siguiente, algo tiene que guardar el contexto y volver a cargarlo. Ese algo es el SecurityContextRepository, y elegir cuál usar es elegir entre una aplicación stateful o stateless.
Dos formas de recordar quién eres
Sección titulada «Dos formas de recordar quién eres»| Stateful (sesión) | Stateless (token) | |
|---|---|---|
| Dónde vive la identidad | En la HttpSession; el cliente guarda la cookie JSESSIONID | En el cliente: cada petición trae Authorization: Bearer … |
| Repositorio | HttpSessionSecurityContextRepository | RequestAttributeSecurityContextRepository (solo dura la petición) |
| Cerrar sesión | Inmediato: se invalida la sesión | El token vale hasta que expira |
| Varias instancias | Spring Session + Redis, o sticky sessions | Trivial |
| Riesgo típico | CSRF, robo de cookie | Robo del token |
| Encaja con | Apps web con servidor, BFF | APIs para SPAs, móviles y servicios |
Repasa la lección de sesiones, cookies y tokens si necesitas el contexto general.
Cómo se carga y cómo se guarda
Sección titulada «Cómo se carga y cómo se guarda»- Petición con
JSESSIONID SecurityContextHolderFilterpide el contexto al repositorio, de forma perezosa- Resto de la cadena y controladorusan el contexto
- Fin de la petición
FilterChainProxylimpia el holder
- Cargar:
SecurityContextHolderFilter, el primero de la cadena, pide el contexto al repositorio solo si alguien lo necesita. - Guardar: desde Spring Security 6 el guardado es explícito. Los filtros de login del framework ya llaman a
saveContext. Pero si autenticas a mano (un controlador de login JSON propio con sesión), tienes que hacerlo tú, o el usuario estará autenticado solo durante esa petición.
@RestControllerclass LoginController {
private final AuthenticationManager authenticationManager; private final SecurityContextRepository contextRepository = new HttpSessionSecurityContextRepository();
LoginController(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; }
@PostMapping("/web/login") ResponseEntity<Void> login(@RequestBody LoginRequest body, HttpServletRequest request, HttpServletResponse response) { Authentication result = authenticationManager.authenticate( UsernamePasswordAuthenticationToken.unauthenticated(body.username(), body.password()));
if (request.getSession(false) != null) { request.changeSessionId(); // contra session fixation } SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(result); SecurityContextHolder.setContext(context); contextRepository.saveContext(context, request, response); // ← sin esto, se olvida return ResponseEntity.noContent().build(); }}Spring Boot no publica un AuthenticationManager como bean; si lo necesitas, decláralo:
@BeanAuthenticationManager authenticationManager(UserDetailsService users, PasswordEncoder encoder) { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(users); provider.setPasswordEncoder(encoder); return new ProviderManager(provider);}Políticas de creación de sesión
Sección titulada «Políticas de creación de sesión»http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));| Política | Comportamiento |
|---|---|
IF_REQUIRED (por defecto) | Crea sesión solo cuando hace falta (p. ej., tras un login de formulario) |
ALWAYS | Crea siempre una sesión |
NEVER | No crea sesiones, pero usa una si ya existe |
STATELESS | Ni crea ni usa sesiones para seguridad. Lo correcto para una API con tokens |
Ataques a la sesión y sus defensas
Sección titulada «Ataques a la sesión y sus defensas»Fijación de sesión (session fixation): el atacante consigue que la víctima use un id de sesión que él conoce, espera a que inicie sesión y lo reutiliza. Defensa por defecto: al autenticar, Spring cambia el id de sesión (changeSessionId). No la desactives.
Robo de cookie: la defensa está en los atributos de la cookie, que se configuran en Spring Boot:
server.servlet.session.cookie.http-only=trueserver.servlet.session.cookie.secure=trueserver.servlet.session.cookie.same-site=laxserver.servlet.session.timeout=30mSesiones concurrentes: limitar cuántas sesiones puede tener un usuario a la vez.
http.sessionManagement(s -> s .sessionConcurrency(c -> c .maximumSessions(1) .maxSessionsPreventsLogin(false))); // false: la sesión nueva expulsa a la vieja
@BeanHttpSessionEventPublisher httpSessionEventPublisher() { // para enterarse de las sesiones que expiran return new HttpSessionEventPublisher();}Escalar: con varias instancias, cada una tiene su propia memoria. Spring Session con Redis (o JDBC) guarda las sesiones en un almacén compartido sin cambiar tu código de seguridad.
LogoutFilter atiende POST /logout por defecto: invalida la sesión, limpia el contexto, borra cookies (como la de remember-me) y redirige. Es POST y no GET por una razón: un GET /logout podría dispararse desde una imagen en otra web (un CSRF de cierre de sesión).
Mini lab · 15 min
Stateful frente a stateless
Objetivo: Ver la diferencia entre una cadena con sesión y una stateless en las cabeceras reales.
Usa el proyecto con dos cadenas de la lección de configuración: una stateless para /api/** y otra con formulario para el resto.
- Inicia sesión en el navegador en
http://localhost:8080/loginy mira en DevTools (pestaña Application → Cookies) la cookieJSESSIONID. ¿Qué atributos tiene? - Anota su valor antes y después de iniciar sesión. ¿Cambia?
- Ejecuta dos veces
curl -i -u ana:ana-password-123 http://localhost:8080/api/hello. ¿ApareceSet-Cookieen la respuesta? - Explica por qué la cadena stateless puede desactivar CSRF y la de sesión no.
Pista
En el punto 2, piensa en qué defensa activa Spring al autenticar.
Mostrar respuestaOcultar respuesta
HttpOnlyyPath=/por defecto. En local, sin HTTPS, no verásSecuresalvo que lo configures; en producción debe estar, junto conSameSite.- Sí cambia: es la protección contra fijación de sesión (
changeSessionId). Un id que el atacante conociera antes del login deja de servir. - No: la cadena es
STATELESS, así que Spring Security no crea sesión. Cada petición se autentica con la cabeceraAuthorizationy se olvida al terminar. - CSRF existe porque el navegador adjunta la cookie de sesión automáticamente. En la cadena stateless la credencial va en
Authorization, que el navegador nunca añade solo, así que una web atacante no puede aprovecharla. La cadena con sesión sí depende de una cookie automática y necesita el token CSRF.