Saltar a contenido

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.