Ir al contenido

Spring SecurityLección 13

Lo nuevo de Spring Security 7

Qué rompe tu código de la 6.x, cómo migrar, y lo realmente nuevo — MFA como parte del modelo de autorización, login por enlace mágico y passkeys.

Avanzado18 min de lectura

Al terminar sabrás

  • Traducir configuración de la 6.x a la 7.x con la tabla de equivalencias.
  • Exigir MFA de forma global o selectiva usando factores como authorities.
  • Configurar login por enlace mágico (one-time token) y passkeys.
  • Conocer las novedades de la 7.1 y la ruta de migración recomendada.

La 7.0 (noviembre de 2025) fue sobre todo una limpieza: eliminó lo que la 6.x había deprecado. Lo realmente nuevo es la autenticación multifactor como parte del modelo de autorización, que la 7.1 (2026) refinó. La ruta de migración recomendada es subir primero a la 6.5, eliminar todos los avisos de deprecación y después saltar a la 7.

Antes (6.x o anterior)Ahora (7.x)
extends WebSecurityConfigurerAdapterBeans SecurityFilterChain (ya en la 6.0)
.and() entre configurersSolo DSL con lambdas
authorizeRequests()authorizeHttpRequests()
AntPathRequestMatcher, MvcRequestMatcherPathPatternRequestMatcher (y ** solo al final del patrón)
AccessDecisionManager, AccessDecisionVoterAuthorizationManager (lo viejo vive en spring-security-access)
AuthorizationManager#checkAuthorizationManager#authorize
http.apply(dsl)http.with(dsl, Customizer.withDefaults())
requiresChannel(c -> c.anyRequest().requiresSecure())redirectToHttps(Customizer.withDefaults())
Jackson 2 (SecurityJackson2Modules)Jackson 3 (SecurityJacksonModules)
Password grant en OAuth2Eliminado: Authorization Code + PKCE o Client Credentials
PKCE opcionalActivado por defecto
typ del JWT sin validarJwtTypeValidator: acepta JWT o ausente
Spring Authorization Server aparteMódulo de Spring Security
OpenSAML 4OpenSAML 5

La idea es elegante: cuando te autenticas con un mecanismo, Spring añade a tu Authentication una authority de factor (FactorGrantedAuthority) con el instante en que la obtuviste. El login con contraseña añade FACTOR_PASSWORD; el enlace mágico, FACTOR_OTT; un passkey, FACTOR_WEBAUTHN. Exigir MFA es, por tanto, una regla de autorización más: “para entrar aquí necesitas estas dos authorities”.

  1. Login con contraseña+ FACTOR_PASSWORD
  2. Regla de /admin/**exige ROLE_ADMIN + FACTOR_PASSWORD + FACTOR_OTT
  3. Redirección al factorenlace mágico → + FACTOR_OTT
  4. Acceso concedido
SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMultiFactorAuthentication(authorities = {
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY })
class SecurityConfig {
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN") // ahora exige ROLE_ADMIN + los dos factores
.anyRequest().authenticated()) // exige los dos factores
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
}

Si el usuario entró con contraseña y le falta el segundo factor, Spring lo redirige automáticamente a la página del factor que falta.

SecurityConfig.java
var mfa = AuthorizationManagerFactories.multiFactor()
.requireFactors(FactorGrantedAuthority.PASSWORD_AUTHORITY, FactorGrantedAuthority.OTT_AUTHORITY)
.build();
var recentPassword = AuthorizationManagerFactories.multiFactor()
.requireFactor(f -> f.passwordAuthority().validDuration(Duration.ofMinutes(30)))
.build();
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").access(mfa.hasRole("ADMIN"))
.requestMatchers("/account/password").access(recentPassword.authenticated()) // re-autenticación
.anyRequest().authenticated());

La 7.1 añadió condiciones: when(...) y withWhen(...) en multiFactor() para exigir MFA solo a ciertos usuarios, y MultiFactorCondition.WEBAUTHN_REGISTERED para pedirlo solo a quien ya registró un passkey.

oneTimeTokenLogin() genera un token de un solo uso y te deja decidir cómo entregarlo (email, SMS). Solo implementas el envío:

MagicLinkSender.java
@Component
class MagicLinkSender implements OneTimeTokenGenerationSuccessHandler {
private final OneTimeTokenGenerationSuccessHandler redirect =
new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
private final MailService mail;
MagicLinkSender(MailService mail) {
this.mail = mail;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken token)
throws IOException, ServletException {
String link = ServletUriComponentsBuilder.fromContextPath(request)
.path("/login/ott")
.queryParam("token", token.getTokenValue())
.toUriString();
mail.send(token.getUsername(), "Tu enlace de acceso", link);
redirect.handle(request, response, token); // "revisa tu correo"
}
}

/ott/sent es una página tuya: créala con un controlador simple y déjala en permitAll(). El formulario para pedir el enlace y la página /login/ott que lo canjea los genera Spring. Los tokens por defecto viven en memoria y caducan a los 5 minutos; en producción usa JdbcOneTimeTokenService.

Los passkeys sustituyen la contraseña por un par de claves guardado en el dispositivo (Touch ID, Windows Hello, el móvil). Son resistentes al phishing porque la clave está ligada al dominio:

SecurityConfig.java
http.webAuthn(w -> w
.rpName("Notas")
.rpId("example.com") // tu dominio
.allowedOrigins("https://app.example.com"));

Necesitas la dependencia de WebAuthn que indica la documentación de tu versión y, en producción, repositorios JDBC para usuarios y credenciales (JdbcPublicKeyCredentialUserEntityRepository, JdbcUserCredentialRepository). Desde la 7.1, el login con passkey publica eventos de autenticación como cualquier otro mecanismo.

  • hasAllAuthorities(...) y hasAllRoles(...) en las reglas (desde la 7.0).
  • InetAddressMatcher, para reglas por IP o rango.
  • ConditionalAuthorizationManager, para componer reglas con condiciones.
  • RestClientOpaqueTokenIntrospector: introspección de tokens opacos sobre RestClient.
  • Soporte de PreFlightRequestFilter para los preflights de CORS.
  • NimbusJwtEncoder.withKeyPair(...) para crear un encoder sin montar un JWKSource a mano.

La 7.2 está en fase de release candidate (octubre de 2026); cuando salga, esta guía se actualizará.

Mini lab · 30 min

MFA solo donde importa

Objetivo: Exigir un segundo factor (enlace mágico) únicamente en una página sensible y ver las authorities resultantes.

  1. En la cadena web activa formLogin + oneTimeTokenLogin y registra un OneTimeTokenGenerationSuccessHandler que, en lugar de enviar email, escriba el enlace en el log.
  2. Crea /web/admin y exígele MFA con AuthorizationManagerFactories.multiFactor() (contraseña + OTT) y hasRole("ADMIN").
  3. Entra con contraseña, visita /web/admin y sigue la redirección; copia el enlace del log y ábrelo.
  4. Crea un endpoint que devuelva authentication.getAuthorities().

Preguntas:

  • ¿Qué authorities ves junto a ROLE_ADMIN tras completar los dos factores?
  • ¿Qué ocurre si visitas /web/admin solo con la contraseña?
  • ¿Cómo exigirías ese mismo segundo factor en un método de servicio, no en una URL?
Pista

Busca authorities que empiecen por FACTOR_.

Mostrar respuestaOcultar respuesta
  • FACTOR_PASSWORD y FACTOR_OTT (cada una como FactorGrantedAuthority, con el instante en que se obtuvo), además de ROLE_ADMIN.
  • La regla falla por falta de FACTOR_OTT. Como estás autenticado pero te falta un factor, Spring no responde con un 403 seco: te redirige a la página del factor que falta (el formulario de enlace mágico).
  • Con una anotación que pida la authority del factor: @PreAuthorize("hasRole('ADMIN') and hasAuthority('FACTOR_OTT')"), o con @EnableMultiFactorAuthentication, que también aplica a la seguridad por método. No hace falta ninguna API nueva: los factores son authorities.