Spring SecurityLección 12
Seguridad reactiva con WebFlux
Los mismos conceptos, otra fontanería. Cómo se protege una aplicación WebFlux, por qué el contexto de seguridad no vive en un ThreadLocal y cómo se prueba con WebTestClient.
Al terminar sabrás
- Traducir la configuración Servlet a su equivalente reactivo.
- Leer el usuario actual con
ReactiveSecurityContextHoldery entender por qué no sirve unThreadLocal. - Proteger métodos que devuelven
MonoyFlux. - Probar reglas de seguridad reactivas con
WebTestClient.
Spring Security funciona igual en espíritu con Spring WebFlux: autenticación, autorización, CSRF, OAuth2 y JWT son los mismos conceptos. Lo que cambia es la fontanería, porque en una aplicación reactiva una petición no tiene un hilo propio.
Las piezas equivalentes
Sección titulada «Las piezas equivalentes»| Servlet (Spring MVC) | Reactivo (WebFlux) |
|---|---|
@EnableWebSecurity | @EnableWebFluxSecurity |
SecurityFilterChain + HttpSecurity | SecurityWebFilterChain + ServerHttpSecurity |
authorizeHttpRequests / requestMatchers / anyRequest | authorizeExchange / pathMatchers / anyExchange |
SecurityContextHolder (ThreadLocal) | ReactiveSecurityContextHolder (contexto de Reactor) |
UserDetailsService | ReactiveUserDetailsService (MapReactiveUserDetailsService en memoria) |
JwtDecoder | ReactiveJwtDecoder |
@EnableMethodSecurity | @EnableReactiveMethodSecurity (métodos que devuelven Mono/Flux) |
MockMvc + jwt() | WebTestClient + mockJwt() |
Configuración
Sección titulada «Configuración»@Configuration@EnableWebFluxSecurity@EnableReactiveMethodSecurityclass SecurityConfig {
@Bean SecurityWebFilterChain api(ServerHttpSecurity http) { http .authorizeExchange(ex -> ex .pathMatchers(HttpMethod.GET, "/api/public/**").permitAll() .pathMatchers("/api/admin/**").hasAuthority("SCOPE_notes:admin") .anyExchange().authenticated()) .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults())) .csrf(ServerHttpSecurity.CsrfSpec::disable); // API stateless con Bearer return http.build(); }}La estructura es la misma que ya conoces: reglas de arriba abajo, un mecanismo de autenticación y la decisión sobre CSRF. Las propiedades de issuer-uri y audiences del resource server también son las mismas.
El usuario actual
Sección titulada «El usuario actual»- Petición
ReactorContextWebFilterescribe el SecurityContext en el contexto de Reactor- Tu pipeline
Mono/Fluxlo lee conReactiveSecurityContextHolder.getContext()
Evita
// En WebFlux esto devuelve null: no hay ThreadLocal fiable@GetMapping("/api/me")Mono<String> me() { return Mono.just(SecurityContextHolder.getContext() .getAuthentication().getName());}Prefiere
@GetMapping("/api/me")Mono<String> me() { return ReactiveSecurityContextHolder.getContext() .map(ctx -> ctx.getAuthentication().getName());}
// …o, más simple, como parámetro:@GetMapping("/api/me2")Mono<String> me2(@AuthenticationPrincipal Jwt jwt) { return Mono.just(jwt.getSubject());}Seguridad por método
Sección titulada «Seguridad por método»Con @EnableReactiveMethodSecurity, @PreAuthorize y @PostAuthorize funcionan sobre métodos que devuelven Mono o Flux; la expresión se evalúa dentro del pipeline:
@PreAuthorize("hasAuthority('SCOPE_notes:admin')")public Flux<Note> all() { return repo.findAll();}
public Mono<Note> get(Long id, String owner) { return repo.findByIdAndOwner(id, owner) .switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)));}Probar con WebTestClient
Sección titulada «Probar con WebTestClient»@WebFluxTest(NoteController.class)@Import(SecurityConfig.class)class NoteRoutesTest {
@Autowired WebTestClient client; @MockitoBean NoteService notes;
@Test void anonimo_recibe401() { client.get().uri("/api/notes").exchange().expectStatus().isUnauthorized(); }
@Test void sinScopeAdmin_recibe403() { client.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("SCOPE_notes:read"))) .get().uri("/api/admin/notes") .exchange() .expectStatus().isForbidden(); }}Mini lab · 30 min
La API de notas, en reactivo
Objetivo: Portar la configuración de seguridad de la API de notas a WebFlux y comprobar que las reglas se mantienen.
- Crea un proyecto con
spring-boot-starter-webflux,spring-boot-starter-securityy el starter de resource server. - Escribe un
SecurityWebFilterChainequivalente a la cadena de la API de la lección de JWT: lectura conSCOPE_notes:read, escritura conSCOPE_notes:write, admin conSCOPE_notes:admin. - Implementa
GET /api/meleyendo el usuario conReactiveSecurityContextHolder. - Escribe la matriz de autorización con
WebTestClientymockJwt().
Preguntas:
- Si en el paso 3 usas
SecurityContextHolder.getContext(), ¿qué obtienes y por qué? - ¿Qué método sustituye a
requestMatchersy cuál aanyRequest? - Tu
ReactiveUserDetailsServiceusa un repositorio JPA. ¿Qué problema tiene?
Pista
Piensa en qué hilo ejecuta tu código en WebFlux y si es el mismo que el que inició la petición.
Mostrar respuestaOcultar respuesta
- Un contexto vacío (autenticación
null):SecurityContextHolderusa unThreadLocal, y en WebFlux el código de la petición puede ejecutarse en cualquier hilo del event loop. El contexto de seguridad vive en el contexto de Reactor y se lee conReactiveSecurityContextHolder.getContext()(o con@AuthenticationPrincipal). pathMatchers(...)sustituye arequestMatchers(...)yanyExchange()aanyRequest(), dentro deauthorizeExchange.- JPA es bloqueante: cada login bloquea un hilo del event loop, y con pocos usuarios simultáneos el servidor deja de responder. Usa R2DBC, o aísla la llamada bloqueante con
Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic()).