Spring WebFlux es la pila reactiva no bloqueante de Spring. Cuando se combina con Spring Security, todos los conceptos del modelo servlet (filtros, AuthenticationManager, UserDetailsService) tienen su contraparte reactiva que opera con Mono y Flux.
La razón para usar WebFlux suele ser alta concurrencia con pocas threads: gateways, proxies, agregadores de APIs. En estos escenarios, la versión bloqueante de Spring Security saturaría el pool de hilos rápidamente.
Diferencias clave con el modelo servlet
| Concepto servlet | Equivalente reactivo |
|---|---|
| SecurityFilterChain | SecurityWebFilterChain |
| AuthenticationManager | ReactiveAuthenticationManager |
| UserDetailsService | ReactiveUserDetailsService |
| SecurityContextHolder | ReactiveSecurityContextHolder |
| OncePerRequestFilter | WebFilter |
| HttpSecurity | ServerHttpSecurity |
La regla mnemotécnica: Reactive para componentes y Server para configuración HTTP.
Dependencias
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Si
spring-boot-starter-webtambién está en el classpath, Spring Boot arranca con servlet por defecto. Quítalo o configuraspring.main.web-application-type=reactive.
SecurityWebFilterChain mínimo
@Configuration
@EnableWebFluxSecurity
public class WebFluxSecurityConfig {
@Bean
SecurityWebFilterChain http(ServerHttpSecurity http) {
http
.authorizeExchange(exchange -> exchange
.pathMatchers("/public/**").permitAll()
.pathMatchers("/admin/**").hasRole("ADMIN")
.anyExchange().authenticated())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.csrf(ServerHttpSecurity.CsrfSpec::disable);
return http.build();
}
}
pathMatchers reemplaza a requestMatchers, authorizeExchange a authorizeHttpRequests y ServerHttpSecurity a HttpSecurity. La estructura es idéntica, solo cambian los nombres.
ReactiveUserDetailsService
@Bean
ReactiveUserDetailsService users(PasswordEncoder encoder, UsuarioRepository repo) {
return username -> repo.findByUsername(username)
.map(u -> User.withUsername(u.getUsername())
.password(u.getPassword())
.roles(u.getRol())
.build());
}
UsuarioRepository es un ReactiveCrudRepository o un R2dbcRepository. La cadena reactiva no bloquea.
No mezcles JDBC con WebFlux. Si tu repositorio es bloqueante, las ventajas de WebFlux desaparecen y suelen aparecer deadlocks sutiles. Usa R2DBC o llama a JDBC sobre
Schedulers.boundedElastic().
Acceder al SecurityContext en un controlador reactivo
ReactiveSecurityContextHolder.getContext() devuelve un Mono<SecurityContext>. Por convención, se compone con el resto de la cadena.
@GetMapping("/me")
public Mono<Map<String, Object>> me() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(auth -> Map.of(
"username", auth.getName(),
"authorities", auth.getAuthorities()
));
}
Para inyectar directamente la Authentication como parámetro, usa @AuthenticationPrincipal:
@GetMapping("/perfil")
public Mono<Perfil> perfil(@AuthenticationPrincipal UserDetails user) {
return service.cargarPerfil(user.getUsername());
}
ReactiveAuthenticationManager personalizado
Para flujos de autenticación a medida (claves API, JWT custom), implementamos un ReactiveAuthenticationManager.
@Component
public class ApiKeyAuthenticationManager implements ReactiveAuthenticationManager {
private final ReactiveApiKeyRepository repo;
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
String key = (String) authentication.getCredentials();
return repo.findByKey(key)
.switchIfEmpty(Mono.error(new BadCredentialsException("API key invalida")))
.map(apiKey -> new UsernamePasswordAuthenticationToken(
apiKey.getOwner(),
key,
List.of(new SimpleGrantedAuthority("ROLE_API"))));
}
}
Y un WebFilter que extrae el header y delega:
@Component
public class ApiKeyWebFilter implements WebFilter {
private final ApiKeyAuthenticationManager authManager;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String key = exchange.getRequest().getHeaders().getFirst("X-Api-Key");
if (key == null) return chain.filter(exchange);
Authentication auth = new UsernamePasswordAuthenticationToken(null, key);
return authManager.authenticate(auth)
.flatMap(authenticated -> chain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authenticated)));
}
}
El truco está en
contextWrite. La cadena reactiva propaga elSecurityContextpor Reactor Context, no porThreadLocal.
Resource Server reactivo con JWT
La configuración con OAuth2 JWT es directa.
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: "https://auth.demo.local/realms/empresa"
@Bean
SecurityWebFilterChain api(ServerHttpSecurity http) {
http
.securityMatcher(ServerWebExchangeMatchers.pathMatchers("/api/**"))
.authorizeExchange(auth -> auth.anyExchange().authenticated())
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
return http.build();
}
Spring crea automáticamente un ReactiveJwtDecoder que descarga el JWK Set del issuer y valida cada token entrante.
Method security reactivo
Para @PreAuthorize en métodos que devuelven Mono o Flux, activa method security reactivo.
@Configuration
@EnableReactiveMethodSecurity
public class MethodSec { }
@Service
public class TareaService {
@PreAuthorize("hasRole('USER')")
public Mono<Tarea> obtener(String id) {
return repo.findById(id);
}
@PreAuthorize("@permisos.puedeEditar(authentication, #id)")
public Mono<Tarea> editar(String id, ActualizarCmd cmd) {
return repo.findById(id).flatMap(t -> repo.save(t.actualizar(cmd)));
}
}
@permisos.puedeEditar debe devolver Mono<Boolean> para encajar en la cadena reactiva.
CORS reactivo
@Bean
CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.demo.local"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return new CorsWebFilter(source);
}
Logout reactivo
.logout(logout -> logout
.logoutUrl("/auth/logout")
.logoutHandler(new HeaderWriterServerLogoutHandler(new ClearSiteDataServerHttpHeadersWriter(
ClearSiteDataServerHttpHeadersWriter.Directive.COOKIES,
ClearSiteDataServerHttpHeadersWriter.Directive.STORAGE
))))
Clear-Site-Data es una cabecera moderna que ordena al navegador eliminar cookies, almacenamiento y caché vinculados al dominio.
Testing reactivo con WebTestClient
@SpringBootTest
@AutoConfigureWebTestClient
class TareaApiTest {
@Autowired WebTestClient client;
@Test
@WithMockUser(username = "ana", roles = "USER")
void usuarioPuedeListarTareas() {
client.get().uri("/api/tareas")
.exchange()
.expectStatus().isOk();
}
@Test
@WithMockUser(roles = "USER")
void userNoAccedeAdmin() {
client.get().uri("/api/admin/usuarios")
.exchange()
.expectStatus().isForbidden();
}
}
mutateWith(SecurityMockServerConfigurers.mockJwt()) permite simular un JWT con claims personalizados sin levantar el authorization server.
client.mutateWith(mockJwt().jwt(jwt -> jwt.claim("scope", "tareas:read")))
.get().uri("/api/tareas")
.exchange().expectStatus().isOk();
Errores frecuentes
- Bloquear dentro de
Mono.map: rompe el modelo reactivo. Usa siempre operadores no bloqueantes (flatMapconMono.fromCallablesobreSchedulers.boundedElasticpara JDBC). - Olvidar
contextWrite: si autenticas en unWebFilterpero no propagas el contexto, el siguiente filtro o controlador no verá la autenticación. - Mezclar
SecurityContextHoldercon WebFlux: elThreadLocalno funciona aquí. UsaReactiveSecurityContextHolder. - Usar
formLogincon SPA: en una API stateless reactiva,formLoginno tiene sentido. Mantén solohttpBasico JWT.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Security es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Spring Security
Explora más contenido relacionado con Spring Security y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Configurar Spring Security sobre WebFlux con ReactiveAuthenticationManager, ServerSecurityContextRepository, ReactiveUserDetailsService y mapear principal con ReactiveSecurityContextHolder.