Ir al contenido

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.

Avanzado15 min de lectura

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

Tus datos
En producción, Spring Security es una capa entre varias.
  • Toda SecurityFilterChain termina en anyRequest().authenticated() o denyAll(), nunca en permitAll().
  • /error está en permitAll() 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 health expuesto públicamente; el resto en otro puerto (management.server.port) o protegido con un rol.
  • Log de org.springframework.security en INFO o WARN: TRACE en producción imprime detalles sensibles.
  • 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.yml versionado.
  • Plan de rotación de claves JWT: publicar varias claves con kid distinto en el JWKS.
  • Access tokens de vida corta (5–15 min) y refresh tokens revocables.
  • Validación de iss y aud configurada, no solo la firma.
  • Cookies de sesión HttpOnly, Secure y SameSite=Lax o Strict.
  • 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.
  • 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.

Spring Security ya publica eventos; solo tienes que escucharlos:

SecurityAudit.java
@Component
class 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:

@Bean
AuthorizationEventPublisher 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ónPor qué es un problemaQué hacer
csrf.disable() copiado en una app con sesiónAbre la puerta a CSRFDejarlo activo o usar csrf.spa()
Filtro JWT hecho a manoOlvida validaciones (alg, exp, aud) y respuestas correctasoauth2ResourceServer().jwt()
@Component en un filtro de seguridadSe ejecuta dos veces, una fuera de la cadenaCrearlo con new o FilterRegistrationBean#setEnabled(false)
"role": "ADMIN" aceptado en el registroEscalada de privilegios por asignación masivaDTOs de entrada sin campos sensibles
owner leído del cuerpo de la peticiónIDOR por diseñoTomarlo de authentication.getName()
JWT en localStorageCualquier XSS lo robaBFF con cookie HttpOnly, o tokens muy cortos
Mensajes distintos para usuario inexistente y contraseña malaEnumeración de usuariosMismo mensaje y mismo tiempo de respuesta
web.ignoring() para endpoints “públicos”Sin cabeceras ni proteccionespermitAll()
Comparar secretos con equalsAtaque de tiempoMessageDigest.isEqual o el PasswordEncoder
Datos personales dentro del JWTEl payload es legibleSolo 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:

SecurityConfig.java
@Bean
SecurityFilterChain 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();
}
application.properties
logging.level.org.springframework.security=TRACE
jwt.secret=mi-clave-super-secreta-123
server.error.include-stacktrace=always

Encuentra 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
  1. CSRF desactivado con formLogin (sesión con cookie): vuelve a activarlo.
  2. CORS que refleja cualquier origen con credenciales (allowedOriginPatterns("*") + allowCredentials(true)): cualquier web puede leer datos de usuarios autenticados. Lista los orígenes exactos.
  3. Actuator entero en permitAll(): puede exponer env, heapdump o loggers. Expón solo health y protege o separa el resto.
  4. anyRequest().permitAll(): todo lo no declarado queda abierto. Cámbialo por authenticated() o denyAll().
  5. TRACE en producción: los logs pueden contener detalles sensibles. Usa INFO o WARN.
  6. 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.
  7. 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.