Ir al contenido

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.

Intermedio14 min de lectura

Al terminar sabrás

  • Explicar el papel del SecurityContextRepository en 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.

Stateful (sesión)Stateless (token)
Dónde vive la identidadEn la HttpSession; el cliente guarda la cookie JSESSIONIDEn el cliente: cada petición trae Authorization: Bearer …
RepositorioHttpSessionSecurityContextRepositoryRequestAttributeSecurityContextRepository (solo dura la petición)
Cerrar sesiónInmediato: se invalida la sesiónEl token vale hasta que expira
Varias instanciasSpring Session + Redis, o sticky sessionsTrivial
Riesgo típicoCSRF, robo de cookieRobo del token
Encaja conApps web con servidor, BFFAPIs para SPAs, móviles y servicios

Repasa la lección de sesiones, cookies y tokens si necesitas el contexto general.

  1. Petición con JSESSIONID
  2. SecurityContextHolderFilterpide el contexto al repositorio, de forma perezosa
  3. Resto de la cadena y controladorusan el contexto
  4. Fin de la peticiónFilterChainProxy limpia 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.
LoginController.java
@RestController
class 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:

@Bean
AuthenticationManager authenticationManager(UserDetailsService users, PasswordEncoder encoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(users);
provider.setPasswordEncoder(encoder);
return new ProviderManager(provider);
}
http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
PolíticaComportamiento
IF_REQUIRED (por defecto)Crea sesión solo cuando hace falta (p. ej., tras un login de formulario)
ALWAYSCrea siempre una sesión
NEVERNo crea sesiones, pero usa una si ya existe
STATELESSNi crea ni usa sesiones para seguridad. Lo correcto para una API con tokens

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:

application.properties
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.same-site=lax
server.servlet.session.timeout=30m

Sesiones 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
@Bean
HttpSessionEventPublisher 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.

  1. Inicia sesión en el navegador en http://localhost:8080/login y mira en DevTools (pestaña Application → Cookies) la cookie JSESSIONID. ¿Qué atributos tiene?
  2. Anota su valor antes y después de iniciar sesión. ¿Cambia?
  3. Ejecuta dos veces curl -i -u ana:ana-password-123 http://localhost:8080/api/hello. ¿Aparece Set-Cookie en la respuesta?
  4. 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
  1. HttpOnly y Path=/ por defecto. En local, sin HTTPS, no verás Secure salvo que lo configures; en producción debe estar, junto con SameSite.
  2. 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.
  3. No: la cadena es STATELESS, así que Spring Security no crea sesión. Cada petición se autentica con la cabecera Authorization y se olvida al terminar.
  4. 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.