SuperAPI AI Directory

gateway-spring

Coding & development

View repository

Updated 10-15 23:33

About this skill

Gateway Spring Boot (apigen-gateway)

Configuration

spring:
  cloud:
    gateway:
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin
        - AddResponseHeader=X-Request-Id, ${random.uuid}
  routes:
    - id: user-service
      uri: lb://user-service
      predicates:
        - Path=/api/users/**
      filters:
        - StripPrefix=1
        - name: CircuitBreaker
          args:
            name: userServiceCB
            fallbackUri: forward:/fallback/users

    - id: order-service
      uri: lb://order-service
      predicates:
        - Path=/api/orders/**
      filters:
        - StripPrefix=1
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 10
            redis-rate-limiter.burstCapacity: 20

  globalcors:
    cors-configurations:
      '[/**]':
        allowedOrigins: "*"
        allowedMethods:
          - GET
          - POST
          - PUT
          - DELETE
        allowedHeaders: "*"

apigen: gateway: enabled: true rate-limit: enabled: true default-limit: 100


## Java Configuration

```java
@Configuration
public class GatewayConfiguration {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // User service
            .route("user-service", r -> r
                .path("/api/users/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Gateway-Source", "apigen")
                    .circuitBreaker(c -> c
                        .setName("userServiceCB")
                        .setFallbackUri("forward:/fallback/users"))
                    .retry(retryConfig -> retryConfig
                        .setRetries(3)
                        .setStatuses(HttpStatus.SERVICE_UNAVAILABLE)
                        .setBackoff(Duration.ofMillis(100), Duration.ofSeconds(1), 2, true)))
                .uri("lb://user-service"))

            // Order service with rate limiting
            .route("order-service", r -> r
                .path("/api/orders/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .requestRateLimiter(c -> c
                        .setRateLimiter(redisRateLimiter())
                        .setKeyResolver(userKeyResolver())))
                .uri("lb://order-service"))

            // Rewrite path
            .route("product-service", r -> r
                .path("/products/**")
                .filters(f -> f
                    .rewritePath("/products/(?<segment>.*)", "/api/v1/products/${segment}"))
                .uri("lb://product-service"))

            .build();
    }

    @Bean
    public RedisRateLimiter redisRateLimiter() {
        return new RedisRateLimiter(10, 20, 1);
    }

    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getHeaders()
                .getFirst("X-User-Id"));
    }
}

Custom Filter

@Component
public class AuthenticationGatewayFilter implements GatewayFilter, Ordered {

    private final JwtService jwtService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = extractToken(exchange.getRequest());

        if (token == null) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        try {
            Claims claims = jwtService.validateToken(token);

            // Add user info to headers for downstream services
            ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
                .header("X-User-Id", claims.getSubject())
                .header("X-User-Roles", String.join(",", claims.get("roles", List.class)))
                .build();

            return chain.filter(exchange.mutate().request(mutatedRequest).build());

        } catch (JwtException e) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
    }

    private String extractToken(ServerHttpRequest request) {
        String auth = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (auth != null && auth.startsWith("Bearer ")) {
            return auth.substring(7);
        }
        return null;
    }

    @Override
    public int getOrder() {
        return -100; // High priority
    }
}

// Register as named filter
@Component
public class AuthenticationGatewayFilterFactory
        extends AbstractGatewayFilterFactory<Object> {

    private final AuthenticationGatewayFilter filter;

    @Override
    public GatewayFilter apply(Object config) {
        return filter;
    }
}

Global Filter

@Component
@Slf4j
public class LoggingGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String requestId = UUID.randomUUID().toString();
        long startTime = System.currentTimeMillis();

        ServerHttpRequest request = exchange.getRequest().mutate()
            .header("X-Request-Id", requestId)
            .build();

        log.info("Gateway Request: {} {} [{}]",
            request.getMethod(),
            request.getPath(),
            requestId);

        return chain.filter(exchange.mutate().request(request).build())
            .then(Mono.fromRunnable(() -> {
                long duration = System.currentTimeMillis() - startTime;
                log.info("Gateway Response: {} {} {} {}ms [{}]",
                    request.getMethod(),
                    request.getPath(),
                    exchange.getResponse().getStatusCode(),
                    duration,
                    requestId);
            }));
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

Circuit Breaker Configuration

@Configuration
public class CircuitBreakerConfiguration {

    @Bean
    public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() {
        return factory -> factory.configureDefault(id ->
            new Resilience4JConfigBuilder(id)
                .circuitBreakerConfig(CircuitBreakerConfig.custom()
                    .slidingWindowSize(10)
                    .failureRateThreshold(50)
                    .waitDurationInOpenState(Duration.ofSeconds(30))
                    .permittedNumberOfCallsInHalfOpenState(5)
                    .build())
                .timeLimiterConfig(TimeLimiterConfig.custom()
                    .timeoutDuration(Duration.ofSeconds(3))
                    .build())
                .build());
    }
}

Fallback Controller

@RestController
@RequestMapping("/fallback")
public class FallbackController {

    @GetMapping("/users")
    public ResponseEntity<Map<String, String>> usersFallback() {
        return ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(Map.of(
                "error", "User service temporarily unavailable",
                "fallback", "true"
            ));
    }

    @GetMapping("/orders")
    public ResponseEntity<Map<String, String>> ordersFallback() {
        return ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(Map.of(
                "error", "Order service temporarily unavailable",
                "fallback", "true"
            ));
    }
}

Service Discovery Integration

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
          predicates:
            - name: Path
              args:
                pattern: "'/api/' + serviceId + '/**'"
          filters:
            - name: StripPrefix
              args:
                parts: 2

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

Rate Limiting with Redis

@Configuration
public class RateLimitConfiguration {

    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> {
            String apiKey = exchange.getRequest().getHeaders()
                .getFirst("X-API-Key");
            return Mono.just(apiKey != null ? apiKey : "anonymous");
        };
    }

    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(
            Objects.requireNonNull(exchange.getRequest().getRemoteAddress())
                .getAddress().getHostAddress());
    }

    @Bean
    public RedisRateLimiter customRateLimiter() {
        // 100 requests per second, burst of 200
        return new RedisRateLimiter(100, 200, 1);
    }
}

Testing

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class GatewayTest {

    @Autowired
    private WebTestClient webClient;

    @Test
    void shouldRouteToUserService() {
        webClient.get()
            .uri("/api/users/123")
            .header("Authorization", "Bearer valid-token")
            .exchange()
            .expectStatus().isOk()
            .expectHeader().exists("X-Request-Id");
    }

    @Test
    void shouldReturn401WithoutToken() {
        webClient.get()
            .uri("/api/users/123")
            .exchange()
            .expectStatus().isUnauthorized();
    }

    @Test
    void shouldReturnFallbackOnCircuitOpen() {
        // Trigger circuit breaker
        IntStream.range(0, 10).forEach(i ->
            webClient.get()
                .uri("/api/users/123")
                .exchange());

        webClient.get()
            .uri("/api/users/123")
            .exchange()
            .expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
            .expectBody()
            .jsonPath("$.fallback").isEqualTo("true");
    }
}

Related Skills

  • api-gateway: API Gateway concepts
  • spring-boot-4: Spring Boot 4.0 patterns