Spring SecurityLección 15
Seguridad en producción
Lo que Spring Security no hace por ti. Lista de verificación antes de desplegar, auditoría con eventos, límites de intentos, secretos y los anti-patrones más frecuentes en código real.
Al terminar sabrás
- Revisar una configuración contra una lista de verificación de producción.
- Auditar logins y accesos denegados escuchando eventos del framework.
- Gestionar secretos, rotación de claves y HTTPS detrás de un proxy.
- Reconocer los anti-patrones más comunes y su corrección.
Spring Security cubre la identidad y el acceso. Una aplicación en producción necesita además cosas que el framework no hace por ti: limitar intentos, guardar los secretos fuera del código, auditar, vigilar y mantener las dependencias al día.
InfraestructuraWAF, red privada, TLS de punta a punta
Abusorate limiting, bloqueo progresivo
Spring Securityautenticación, autorización, cabeceras, CSRF
Aplicaciónvalidación, DTOs, consultas parametrizadas
Observabilidadauditoría, alertas, dependencias al día
Lista de verificación antes de desplegar
Sección titulada «Lista de verificación antes de desplegar»Configuración
Sección titulada «Configuración»- Toda
SecurityFilterChaintermina enanyRequest().authenticated()odenyAll(), nunca enpermitAll(). -
/errorestá enpermitAll()y los errores no incluyen trazas (server.error.include-stacktrace=never). - CSRF desactivado solo en cadenas sin cookies de autenticación.
- CORS con orígenes explícitos; nunca
*con credenciales. - Actuator: solo
healthexpuesto públicamente; el resto en otro puerto (management.server.port) o protegido con un rol. - Log de
org.springframework.securityenINFOoWARN:TRACEen producción imprime detalles sensibles.
Credenciales y secretos
Sección titulada «Credenciales y secretos»- Contraseñas con
DelegatingPasswordEncoder(bcrypt o Argon2); cero{noop}. - Claves de firma, client secrets y contraseñas de base de datos en variables de entorno o en un gestor de secretos (AWS Secrets Manager, OCI Vault, HashiCorp Vault…), nunca en un
application.ymlversionado. - Plan de rotación de claves JWT: publicar varias claves con
kiddistinto en el JWKS.
Tokens y sesiones
Sección titulada «Tokens y sesiones»- Access tokens de vida corta (5–15 min) y refresh tokens revocables.
- Validación de
issyaudconfigurada, no solo la firma. - Cookies de sesión
HttpOnly,SecureySameSite=LaxoStrict. - HTTPS de punta a punta. Detrás de un proxy o balanceador:
server.forward-headers-strategy=framework, para que Spring sepa que la petición original era HTTPS.
Defensa en profundidad
Sección titulada «Defensa en profundidad»- Rate limiting en login, registro y emisión de tokens (Bucket4j, Redis o el API gateway), contra fuerza bruta y credential stuffing.
- Bloqueo o retraso progresivo tras N intentos fallidos.
- Auditoría de logins y accesos denegados, con usuario, IP y hora.
- Escaneo de dependencias en CI (Dependabot, OWASP Dependency-Check, Snyk) y parches de Spring Boot al menos mensuales.
Auditar con eventos
Sección titulada «Auditar con eventos»Spring Security ya publica eventos; solo tienes que escucharlos:
@Componentclass SecurityAudit {
private static final Logger log = LoggerFactory.getLogger("security.audit");
@EventListener void onSuccess(AuthenticationSuccessEvent e) { log.info("login ok user={}", e.getAuthentication().getName()); }
@EventListener void onFailure(AbstractAuthenticationFailureEvent e) { log.warn("login failed user={} reason={}", e.getAuthentication().getName(), e.getException().getClass().getSimpleName()); }
@EventListener void onDenied(AuthorizationDeniedEvent<?> e) { log.warn("access denied user={}", e.getAuthentication().get().getName()); }}Los eventos de autorización denegada necesitan un publicador:
@BeanAuthorizationEventPublisher authorizationEventPublisher(ApplicationEventPublisher publisher) { return new SpringAuthorizationEventPublisher(publisher);}Anti-patrones que encontrarás en código real
Sección titulada «Anti-patrones que encontrarás en código real»| Anti-patrón | Por qué es un problema | Qué hacer |
|---|---|---|
csrf.disable() copiado en una app con sesión | Abre la puerta a CSRF | Dejarlo activo o usar csrf.spa() |
| Filtro JWT hecho a mano | Olvida validaciones (alg, exp, aud) y respuestas correctas | oauth2ResourceServer().jwt() |
@Component en un filtro de seguridad | Se ejecuta dos veces, una fuera de la cadena | Crearlo con new o FilterRegistrationBean#setEnabled(false) |
"role": "ADMIN" aceptado en el registro | Escalada de privilegios por asignación masiva | DTOs de entrada sin campos sensibles |
owner leído del cuerpo de la petición | IDOR por diseño | Tomarlo de authentication.getName() |
JWT en localStorage | Cualquier XSS lo roba | BFF con cookie HttpOnly, o tokens muy cortos |
| Mensajes distintos para usuario inexistente y contraseña mala | Enumeración de usuarios | Mismo mensaje y mismo tiempo de respuesta |
web.ignoring() para endpoints “públicos” | Sin cabeceras ni protecciones | permitAll() |
Comparar secretos con equals | Ataque de tiempo | MessageDigest.isEqual o el PasswordEncoder |
| Datos personales dentro del JWT | El payload es legible | Solo identificadores y scopes |
OWASP API Security Top 10, mapeado a esta guía
Sección titulada «OWASP API Security Top 10, mapeado a esta guía»Los tres primeros riesgos de la lista de OWASP para APIs son de autorización y autenticación, y los tres están cubiertos aquí:
- API1 – Broken Object Level Authorization (IDOR): autorización por dueño.
- API2 – Broken Authentication: autenticación, JWT y OAuth2.
- API3 – Broken Object Property Level Authorization (asignación masiva y exponer campos de más): DTOs de entrada y salida.
- API5 – Broken Function Level Authorization: reglas por URL y la matriz de autorización.
Mini lab · 20 min
Revisión antes del despliegue
Objetivo: Encontrar los problemas de una configuración real usando la lista de verificación.
Un compañero te pide revisar esta configuración antes de desplegar:
@BeanSecurityFilterChain chain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .cors(cors -> cors.configurationSource(request -> { CorsConfiguration c = new CorsConfiguration(); c.setAllowedOriginPatterns(List.of("*")); c.setAllowCredentials(true); c.setAllowedMethods(List.of("*")); return c; })) .authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().permitAll()) .formLogin(Customizer.withDefaults()); return http.build();}logging.level.org.springframework.security=TRACEjwt.secret=mi-clave-super-secreta-123server.error.include-stacktrace=alwaysEncuentra al menos seis problemas y propón la corrección de cada uno.
Pista
Recorre la lista de verificación sección por sección: configuración, secretos, tokens y sesiones.
Mostrar respuestaOcultar respuesta
- CSRF desactivado con
formLogin(sesión con cookie): vuelve a activarlo. - CORS que refleja cualquier origen con credenciales (
allowedOriginPatterns("*")+allowCredentials(true)): cualquier web puede leer datos de usuarios autenticados. Lista los orígenes exactos. - Actuator entero en
permitAll(): puede exponerenv,heapdumpologgers. Expón solohealthy protege o separa el resto. anyRequest().permitAll(): todo lo no declarado queda abierto. Cámbialo porauthenticated()odenyAll().TRACEen producción: los logs pueden contener detalles sensibles. UsaINFOoWARN.- Secreto en el fichero de propiedades (y además débil): muévelo a una variable de entorno o a un gestor de secretos, genera una clave aleatoria larga y planifica su rotación.
- Stack traces en las respuestas:
server.error.include-stacktrace=never.
Y faltan cosas que no se ven en el código: rate limiting en el login, auditoría de eventos y escaneo de dependencias.