Contents

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!-- Optional: Eureka for service-discovery routing --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> # application.yml — route definitions spring: cloud: gateway: routes: - id: order-service-route uri: http://order-service:8081 # downstream service predicates: - Path=/api/orders/** # match requests to /api/orders/** filters: - StripPrefix=1 # remove /api prefix before forwarding - id: user-service-route uri: http://user-service:8082 predicates: - Path=/api/users/** - Method=GET,POST filters: - RewritePath=/api/users/(?<segment>.*), /users/${segment} // Equivalent Java DSL — RouteLocatorBuilder @Configuration public class GatewayConfig { @Bean public RouteLocator routes(RouteLocatorBuilder builder) { return builder.routes() .route("order-service", r -> r .path("/api/orders/**") .filters(f -> f .stripPrefix(1) .addRequestHeader("X-Gateway", "spring-cloud-gateway")) .uri("http://order-service:8081")) .route("user-service", r -> r .path("/api/users/**") .and().method(HttpMethod.GET, HttpMethod.POST) .filters(f -> f.rewritePath("/api/users/(?<s>.*)", "/users/${s}")) .uri("http://user-service:8082")) .build(); } }

Predicates are conditions that must be true for a route to match a request. Multiple predicates are ANDed together.

spring: cloud: gateway: routes: - id: examples uri: http://backend:8080 predicates: # Path matching - Path=/v1/**, /v2/** # HTTP method - Method=GET,POST,PUT # Header presence and value - Header=X-Request-Id, \d+ # header value matches regex - Header=Authorization # header just needs to exist # Query parameter - Query=version, v[12] # ?version=v1 or ?version=v2 # Host - Host=**.mycompany.com,api.mycompany.io # Remote address (IP allowlist) - RemoteAddr=192.168.1.0/24 # Time-based routing - Between=2024-01-01T00:00:00Z[UTC], 2024-12-31T23:59:59Z[UTC] - After=2024-06-01T00:00:00Z[UTC] # Weight-based (canary deployments) - id: service-v1 uri: http://service-v1:8080 predicates: - Path=/api/products/** - Weight=service-group, 90 # 90% of traffic - id: service-v2 uri: http://service-v2:8080 predicates: - Path=/api/products/** - Weight=service-group, 10 # 10% to new version
spring: cloud: gateway: routes: - id: demo uri: http://backend:8080 predicates: - Path=/api/** filters: # Path manipulation - StripPrefix=1 # remove first path segment - PrefixPath=/internal # add prefix before forwarding - RewritePath=/api/(?<seg>.*), /${seg} # regex rewrite # Header manipulation - AddRequestHeader=X-Source, gateway - AddResponseHeader=X-Response-Time, 100ms - RemoveRequestHeader=X-Internal-Token - RemoveResponseHeader=X-Powered-By # Redirects & rewrites - RedirectTo=302, https://new.example.com # Retry on failure - name: Retry args: retries: 3 statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE methods: GET,HEAD backoff: firstBackoff: 50ms maxBackoff: 500ms factor: 2 # Request size limit - name: RequestSize args: maxSize: 5MB # Default filters applied to ALL routes default-filters: - AddResponseHeader=X-Gateway-Version, 1.0 - name: Retry args: retries: 2 statuses: SERVICE_UNAVAILABLE // Global filter — applies to every request through the gateway @Component @Slf4j public class RequestLoggingFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest req = exchange.getRequest(); String requestId = UUID.randomUUID().toString(); log.info("[{}] {} {}", requestId, req.getMethod(), req.getURI()); // Mutate request — add a tracing header ServerWebExchange mutated = exchange.mutate() .request(req.mutate() .header("X-Request-Id", requestId) .build()) .build(); long start = System.currentTimeMillis(); return chain.filter(mutated) .doFinally(signal -> log.info("[{}] completed in {}ms", requestId, System.currentTimeMillis() - start)); } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE - 1; } } // Route-scoped filter factory @Component public class ApiKeyFilterFactory extends AbstractGatewayFilterFactory<ApiKeyFilterFactory.Config> { public ApiKeyFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { String apiKey = exchange.getRequest().getHeaders().getFirst("X-Api-Key"); if (!config.getValidKey().equals(apiKey)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); }; } public static class Config { private String validKey; /* getter/setter */ } } # Use the custom filter in a route spring: cloud: gateway: routes: - id: protected-route uri: http://internal-service:8080 predicates: - Path=/internal/** filters: - ApiKey=secret-api-key-value # references ApiKeyFilterFactory <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> @Configuration public class RateLimitConfig { // Key resolver — rate limit per authenticated user (falls back to IP) @Bean public KeyResolver userKeyResolver() { return exchange -> { String user = exchange.getRequest().getHeaders().getFirst("X-User-Id"); if (user != null) return Mono.just(user); return Mono.just( Objects.requireNonNull(exchange.getRequest().getRemoteAddress()) .getAddress().getHostAddress()); }; } } spring: redis: host: localhost port: 6379 cloud: gateway: routes: - id: rate-limited-api uri: http://api-service:8080 predicates: - Path=/api/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 # tokens added per second redis-rate-limiter.burstCapacity: 20 # max burst tokens redis-rate-limiter.requestedTokens: 1 # tokens per request key-resolver: "#{@userKeyResolver}" # SpEL bean reference The RequestRateLimiter uses the token bucket algorithm implemented in Redis via a Lua script, making it atomic and cluster-safe. When the limit is exceeded, the gateway returns 429 Too Many Requests with X-RateLimit-Remaining: 0 headers. <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId> </dependency> spring: cloud: gateway: routes: - id: order-service uri: http://order-service:8081 predicates: - Path=/api/orders/** filters: - name: CircuitBreaker args: name: orderServiceCB fallbackUri: forward:/fallback/orders # internal gateway route # Fallback route - id: orders-fallback uri: no://op predicates: - Path=/fallback/orders filters: - SetStatus=503 - name: SetResponseHeader args: name: Content-Type value: application/json resilience4j: circuitbreaker: instances: orderServiceCB: slidingWindowSize: 10 failureRateThreshold: 50 waitDurationInOpenState: 10s permittedNumberOfCallsInHalfOpenState: 3 // Fallback controller inside the gateway @RestController public class FallbackController { @GetMapping("/fallback/orders") public Mono<ResponseEntity<Map<String, String>>> ordersFallback(ServerWebExchange exchange) { String cause = exchange.getAttribute(ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR) != null ? "Circuit breaker open" : "Service unavailable"; return Mono.just(ResponseEntity.status(503) .body(Map.of("error", cause, "message", "Order service is temporarily unavailable"))); } } @Component @RequiredArgsConstructor public class JwtAuthenticationFilter implements GlobalFilter, Ordered { private final JwtDecoder jwtDecoder; // Spring Security's JwtDecoder bean private static final List<String> PUBLIC_PATHS = List.of( "/api/auth/login", "/api/auth/register", "/actuator/health"); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path = exchange.getRequest().getPath().value(); if (PUBLIC_PATHS.stream().anyMatch(path::startsWith)) { return chain.filter(exchange); // skip auth for public paths } String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); if (authHeader == null || !authHeader.startsWith("Bearer ")) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } String token = authHeader.substring(7); return Mono.fromCallable(() -> jwtDecoder.decode(token)) .onErrorResume(JwtException.class, ex -> { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return Mono.empty(); }) .flatMap(jwt -> { // Forward user info in headers to downstream services ServerWebExchange mutated = exchange.mutate() .request(exchange.getRequest().mutate() .header("X-User-Id", jwt.getSubject()) .header("X-User-Roles", String.join(",", jwt.getClaimAsStringList("roles"))) .build()) .build(); return chain.filter(mutated); }); } @Override public int getOrder() { return -100; } // run early, before routing filters } # With Eureka — route by service ID, no hardcoded IPs spring: cloud: gateway: discovery: locator: enabled: true # auto-create routes for all Eureka services lower-case-service-id: true # /order-service/** (not /ORDER-SERVICE/**) # Or define explicit routes using lb:// scheme (load-balanced) routes: - id: order-service uri: lb://order-service # resolves via Eureka + client-side load balancer predicates: - Path=/api/orders/** filters: - StripPrefix=1 - id: payment-service uri: lb://payment-service predicates: - Path=/api/payments/** filters: - StripPrefix=1 The lb:// URI scheme tells Spring Cloud Gateway to use the Spring Cloud LoadBalancer (or Ribbon in older versions) to resolve the service IP from the discovery registry, and round-robin across healthy instances automatically.