Contents
- Setup & Basic Route Definition
- Route Predicates
- Built-in GatewayFilters
- Custom Global & Route Filters
- Rate Limiting with Redis
- Circuit Breaker Integration
- JWT Authentication Filter
- Service Discovery Routing
<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.