C4 Component — Spring WebFlux¶
El microservicio Spring WebFlux actúa como Resource Server en la plataforma: recibe peticiones HTTP autenticadas con JWT emitidos por Keycloak, valida el token localmente y ejecuta la lógica de negocio de forma completamente reactiva. Este documento descompone sus componentes internos y describe el flujo de una petición desde el router hasta la respuesta.
Diagrama de componentes internos¶
flowchart TB
subgraph WebFlux["Spring WebFlux — Resource Server (Java 21 / Spring Boot 4.x)"]
subgraph Web["Capa Web Reactiva"]
RF["RouterFunctions\n────────────────\n• Definición funcional de rutas\n• Sin @Controller / @RequestMapping\n• Composición de rutas por módulo"]
HF["HandlerFunctions\n────────────────\n• Reciben ServerRequest\n• Devuelven Mono<ServerResponse>\n• Delegan a Use Cases"]
end
subgraph Security["Capa de Seguridad Reactiva"]
SFC["SecurityWebFilterChain\n────────────────\n• Configura rutas públicas y protegidas\n• Integra BearerTokenAuthFilter\n• CSRF deshabilitado (stateless)"]
JD["ReactiveJwtDecoder\n────────────────\n• Descarga JWKS de Keycloak\n• Cachea claves públicas RSA\n• Valida firma, exp, iss, aud"]
AM["ReactiveAuthorizationManager\n────────────────\n• Evalúa scopes y roles\n• hasAuthority('SCOPE_partner.read')\n• Decisiones por ruta"]
end
subgraph Application["Capa de Aplicación"]
UC["Use Cases / Services\n────────────────\n• Lógica de negocio pura\n• Sin dependencias de framework\n• Retornan Mono / Flux"]
Port["Ports (interfaces)\n────────────────\n• PartnerRepository (puerto)\n• NotificationPort (puerto)\n• Independientes de implementación"]
end
subgraph Infra["Capa de Infraestructura"]
R2DBC["R2DBC Repository\n────────────────\n• Implementa puertos\n• DatabaseClient reactivo\n• PostgreSQL 16"]
KC_Client["Keycloak Client\n────────────────\n• Admin REST API (opcional)\n• WebClient reactivo\n• Gestión de tokens de servicio"]
end
RF --> SFC
SFC --> JD
JD --> AM
AM --> HF
HF --> UC
UC --> Port
Port --> R2DBC
Port --> KC_Client
end
Traefik([Traefik\nReverse Proxy]) -->|HTTP interno| RF
R2DBC -->|R2DBC protocol| DB[(PostgreSQL 16)]
JD -->|GET /certs| KC([Keycloak\nJWKS endpoint])
KC_Client -->|Admin REST API| KC
Tabla de componentes¶
| Componente | Responsabilidad | Tecnología / Tipo |
|---|---|---|
| RouterFunctions | Define el mapping de rutas HTTP de forma funcional. Las rutas se componen desde múltiples módulos y se registran como un único bean. | RouterFunction<ServerResponse> |
| HandlerFunctions | Implementa la lógica de cada endpoint. Extrae datos del ServerRequest, llama al use case y construye la respuesta reactiva. |
HandlerFunction<ServerResponse> |
| SecurityWebFilterChain | Configura qué rutas requieren autenticación y qué scopes/roles son necesarios. Integra el filtro de Bearer token automáticamente. | SecurityWebFilterChain (Spring Security 7) |
| ReactiveJwtDecoder | Valida los JWT recibidos usando las claves públicas del JWKS de Keycloak. Soporta rotación de claves (kid lookup). |
NimbusReactiveJwtDecoder |
| ReactiveAuthorizationManager | Evalúa si el JWT del usuario tiene los scopes o roles necesarios para acceder a cada ruta. | ReactiveAuthorizationManager<AuthorizationContext> |
| Use Cases | Implementa la lógica de negocio pura. No conoce HTTP ni Spring. Recibe y retorna tipos de dominio. | POJO con Mono/Flux (Project Reactor) |
| Ports | Interfaces que definen los contratos entre la capa de aplicación y la infraestructura. Permiten sustituir implementaciones sin modificar la lógica. | Interfaces Java |
| R2DBC Repository | Implementa el port de acceso a datos. Usa DatabaseClient o R2dbcEntityTemplate para queries reactivos. |
Spring Data R2DBC |
Configuración de seguridad¶
// SecurityConfig.java — Spring Security 7 + WebFlux
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable) // API stateless
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/actuator/health", "/actuator/info").permitAll()
.pathMatchers(HttpMethod.GET, "/api/partners/**")
.hasAuthority("SCOPE_partner.read")
.pathMatchers(HttpMethod.POST, "/api/partners/**")
.hasAuthority("SCOPE_partner.write")
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtDecoder(jwtDecoder()))
)
.build();
}
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder
.withJwkSetUri("https://auth.ejemplo.com/realms/realm-partners"
+ "/protocol/openid-connect/certs")
.build();
}
}
Configuración de RouterFunctions¶
// PartnerRouter.java
@Configuration
public class PartnerRouter {
@Bean
public RouterFunction<ServerResponse> partnerRoutes(PartnerHandler handler) {
return RouterFunctions.route()
.GET("/api/partners", handler::findAll)
.GET("/api/partners/{id}", handler::findById)
.POST("/api/partners", handler::create)
.PUT("/api/partners/{id}", handler::update)
.DELETE("/api/partners/{id}", handler::delete)
.build();
}
}
Implementación de un Handler¶
// PartnerHandler.java
@Component
@RequiredArgsConstructor
public class PartnerHandler {
private final FindPartnerUseCase findPartnerUseCase;
public Mono<ServerResponse> findById(ServerRequest request) {
String id = request.pathVariable("id");
return findPartnerUseCase.execute(id)
.flatMap(partner -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(partner))
.switchIfEmpty(ServerResponse.notFound().build())
.onErrorResume(ValidationException.class, ex ->
ServerResponse.badRequest().bodyValue(ex.getMessage()));
}
}
Flujo de una petición autenticada¶
sequenceDiagram
actor Cliente
participant RF as RouterFunctions
participant SFC as SecurityWebFilterChain
participant JD as ReactiveJwtDecoder
participant AM as AuthorizationManager
participant H as PartnerHandler
participant UC as FindPartnerUseCase
participant Repo as R2DBC Repository
Cliente->>RF: GET /api/partners/123\nAuthorization: Bearer eyJhbGc...
RF->>SFC: Petición entra al filtro de seguridad
SFC->>JD: Extraer y validar JWT del header
JD->>JD: Verificar firma RS256 con clave JWKS (cacheada)
JD->>JD: Validar exp, iss=auth.ejemplo.com, aud
JD-->>SFC: JwtAuthenticationToken { sub, scopes, roles }
SFC->>AM: ¿Tiene SCOPE_partner.read?
AM-->>SFC: Autorizado
SFC->>H: Delegar a PartnerHandler.findById()
H->>UC: FindPartnerUseCase.execute("123")
UC->>Repo: findById("123") → Mono<Partner>
Repo->>Repo: SELECT * FROM partners WHERE id = $1 (R2DBC)
Repo-->>UC: Partner domain object
UC-->>H: Mono<Partner>
H-->>Cliente: 200 OK { "id": "123", "name": "..." }
Validación de claims JWT en el contexto reactivo¶
// Acceder al JWT en un handler de forma reactiva
public Mono<ServerResponse> findById(ServerRequest request) {
return request.principal()
.cast(JwtAuthenticationToken.class)
.flatMap(auth -> {
Jwt jwt = auth.getToken();
String subject = jwt.getSubject();
List<String> scopes = jwt.getClaimAsStringList("scope");
return findPartnerUseCase.execute(request.pathVariable("id"), subject);
})
.flatMap(partner -> ServerResponse.ok().bodyValue(partner));
}
Propiedades de configuración¶
# application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.ejemplo.com/realms/realm-partners
# jwk-set-uri: se infiere automáticamente desde issuer-uri
r2dbc:
url: r2dbc:postgresql://postgres:5432/partnerdb
username: ${DB_USER}
password: ${DB_PASSWORD}
server:
port: 8080
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: when-authorized
Cacheo de JWKS
NimbusReactiveJwtDecoder cachea las claves públicas por defecto durante 5 minutos. Si Keycloak rota claves, el decoder detecta un kid desconocido y refresca automáticamente el JWKS sin necesidad de reiniciar la aplicación.
Validación de issuer
Cuando se configura issuer-uri, Spring Security valida que el claim iss del JWT coincida exactamente con la URI del realm. Un JWT de realm-internal será rechazado por un Resource Server configurado para realm-partners.