google-site-verification: google61fe8ba583a51912.html
API Gateway with Spring Cloud Gateway in Java

Spring Cloud Gateway provides a library for building API Gateways on top of Spring WebFlux. It offers a simple, yet effective way to route to APIs and provide cross-cutting concerns like security, monitoring, and resiliency.

Key Features

  • Built on Spring Framework 6, Spring Boot 3, and Project Reactor
  • Predicate and Filter-based routing
  • Circuit Breaker integration
  • Spring Cloud DiscoveryClient integration
  • Easy to write predicates and filters
  • Request rate limiting
  • Path rewriting

Dependencies Setup

Maven Dependencies

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.0</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>api-gateway</artifactId> <version>1.0.0</version> <properties> <java.version>17</java.version> <spring-cloud.version>2023.0.0</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!-- Service Discovery --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- Circuit Breaker --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId> </dependency> <!-- Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <!-- Config --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <!-- Actuator --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>

Basic Configuration

Application Configuration

# application.yml server: port: 8080 spring: application: name: api-gateway cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true routes: - id: user-service uri: lb://USER-SERVICE predicates: - Path=/api/users/** filters: - StripPrefix=1 - name: CircuitBreaker args: name: userService fallbackUri: forward:/fallback/user-service - id: product-service uri: lb://PRODUCT-SERVICE predicates: - Path=/api/products/** filters: - StripPrefix=1 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1 - id: order-service uri: lb://ORDER-SERVICE predicates: - Path=/api/orders/** filters: - StripPrefix=1 - name: JwtAuthentication - id: auth-service uri: lb://AUTH-SERVICE predicates: - Path=/api/auth/** eureka: client: service-url: defaultZone: http://localhost:8761/eureka instance: prefer-ip-address: true management: endpoints: web: exposure: include: health,info,metrics,gateway endpoint: gateway: enabled: true resilience4j: circuitbreaker: instances: userService: register-health-indicator: true failure-rate-threshold: 50 slow-call-rate-threshold: 50 slow-call-duration-threshold: 2s permitted-number-of-calls-in-half-open-state: 3 sliding-window-size: 10 minimum-number-of-calls: 5 wait-duration-in-open-state: 5s

Main Application Class

package com.example.apigateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class ApiGatewayApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayApplication.class, args); } }

Custom Filters and Predicates

Example 1: JWT Authentication Filter

package com.example.apigateway.filters; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.crypto.SecretKey; import java.util.List; @Component public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAuthenticationFilter.Config> { @Value("${jwt.secret}") private String jwtSecret; public JwtAuthenticationFilter() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { if (!exchange.getRequest().getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) { return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED); } String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); if (authHeader == null || !authHeader.startsWith("Bearer ")) { return onError(exchange, "Invalid authorization header", HttpStatus.UNAUTHORIZED); } String token = authHeader.substring(7); try { if (isTokenValid(token)) { String username = getUsernameFromToken(token); List<String> roles = getRolesFromToken(token); // Add headers for downstream services ServerWebExchange mutatedExchange = exchange.mutate() .request(builder -> builder .header("X-User-Id", username) .header("X-User-Roles", String.join(",", roles)) ) .build(); return chain.filter(mutatedExchange); } else { return onError(exchange, "Invalid token", HttpStatus.UNAUTHORIZED); } } catch (Exception e) { return onError(exchange, "Token validation failed", HttpStatus.UNAUTHORIZED); } }; } private boolean isTokenValid(String token) { try { SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes()); Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token); return true; } catch (Exception e) { return false; } } private String getUsernameFromToken(String token) { SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes()); Claims claims = Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } private List<String> getRolesFromToken(String token) { SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes()); Claims claims = Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); return claims.get("roles", List.class); } private Mono<Void> onError(ServerWebExchange exchange, String error, HttpStatus status) { exchange.getResponse().setStatusCode(status); return exchange.getResponse().setComplete(); } public static class Config { // Configuration properties if needed } }

Example 2: Logging Filter

package com.example.apigateway.filters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.time.Duration; import java.time.Instant; @Component public class LoggingFilter implements GlobalFilter, Ordered { private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { Instant startTime = Instant.now(); return chain.filter(exchange).then(Mono.fromRunnable(() -> { Instant endTime = Instant.now(); Duration duration = Duration.between(startTime, endTime); String path = exchange.getRequest().getPath().value(); String method = exchange.getRequest().getMethod().name(); int status = exchange.getResponse().getStatusCode() != null ? exchange.getResponse().getStatusCode().value() : 500; logger.info("{} {} - Status: {} - Duration: {}ms", method, path, status, duration.toMillis()); })); } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }

Example 3: Rate Limiting Filter

package com.example.apigateway.filters; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.data.redis.core.ReactiveRedisTemplate; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.time.Instant; import java.util.concurrent.TimeUnit; @Component public class CustomRateLimiterFilter extends AbstractGatewayFilterFactory<CustomRateLimiterFilter.Config> { @Autowired private ReactiveRedisTemplate<String, String> redisTemplate; public CustomRateLimiterFilter() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { String clientIp = getClientIp(exchange); String key = "rate_limit:" + clientIp + ":" + Instant.now().getEpochSecond(); return redisTemplate.opsForValue().increment(key) .flatMap(count -> { if (count == 1) { // Set expiration on first request return redisTemplate.expire(key, config.getWindowInSeconds(), TimeUnit.SECONDS) .then(Mono.just(count)); } return Mono.just(count); }) .flatMap(count -> { if (count > config.getMaxRequests()) { exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS); return exchange.getResponse().setComplete(); } return chain.filter(exchange); }); }; } private String getClientIp(ServerWebExchange exchange) { String xForwardedFor = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return exchange.getRequest().getRemoteAddress() != null ? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress() : "unknown"; } public static class Config { private int maxRequests = 10; private int windowInSeconds = 60; public int getMaxRequests() { return maxRequests; } public void setMaxRequests(int maxRequests) { this.maxRequests = maxRequests; } public int getWindowInSeconds() { return windowInSeconds; } public void setWindowInSeconds(int windowInSeconds) { this.windowInSeconds = windowInSeconds; } } }

Fallback Handlers

Example 4: Circuit Breaker Fallback Controller

package com.example.apigateway.fallback; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/fallback") public class FallbackController { @GetMapping("/user-service") public Mono<ResponseEntity<Map<String, Object>>> userServiceFallback() { return Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(createFallbackResponse("User service is temporarily unavailable"))); } @GetMapping("/product-service") public Mono<ResponseEntity<Map<String, Object>>> productServiceFallback() { return Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(createFallbackResponse("Product service is temporarily unavailable"))); } @GetMapping("/order-service") public Mono<ResponseEntity<Map<String, Object>>> orderServiceFallback() { return Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(createFallbackResponse("Order service is temporarily unavailable"))); } private Map<String, Object> createFallbackResponse(String message) { Map<String, Object> response = new HashMap<>(); response.put("success", false); response.put("message", message); response.put("timestamp", System.currentTimeMillis()); return response; } }

Security Configuration

Example 5: Gateway Security Configuration

package com.example.apigateway.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsConfigurationSource; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import java.util.Arrays; @Configuration @EnableWebFluxSecurity public class SecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http .csrf(ServerHttpSecurity.CsrfSpec::disable) .authorizeExchange(exchanges -> exchanges .pathMatchers("/api/auth/**").permitAll() .pathMatchers("/actuator/**").permitAll() .pathMatchers("/fallback/**").permitAll() .pathMatchers("/api/users/register").permitAll() .pathMatchers("/api/users/login").permitAll() .anyExchange().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwtAuthenticationConverter(new JwtAuthenticationConverter())) ) .cors(cors -> cors.configurationSource(corsConfigurationSource())) .build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOriginPatterns(Arrays.asList("*")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }

Dynamic Route Configuration

Example 6: Dynamic Route Repository

package com.example.apigateway.config; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionRepository; import org.springframework.data.redis.core.ReactiveRedisTemplate; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Map; @Component public class RedisRouteDefinitionRepository implements RouteDefinitionRepository { private final ReactiveRedisTemplate<String, RouteDefinition> redisTemplate; private static final String ROUTE_KEY = "gateway_routes"; public RedisRouteDefinitionRepository(ReactiveRedisTemplate<String, RouteDefinition> redisTemplate) { this.redisTemplate = redisTemplate; } @Override public Flux<RouteDefinition> getRouteDefinitions() { return redisTemplate.opsForHash().values(ROUTE_KEY) .cast(RouteDefinition.class); } @Override public Mono<Void> save(Mono<RouteDefinition> route) { return route.flatMap(routeDefinition -> redisTemplate.opsForHash().put(ROUTE_KEY, routeDefinition.getId(), routeDefinition) ).then(); } @Override public Mono<Void> delete(Mono<String> routeId) { return routeId.flatMap(id -> redisTemplate.opsForHash().remove(ROUTE_KEY, id) ).then(); } }

Example 7: Route Refresh Controller

package com.example.apigateway.controller; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @RestController @RequestMapping("/gateway") public class RouteRefreshController implements ApplicationEventPublisherAware { private ApplicationEventPublisher publisher; @PostMapping("/refresh") public Mono<String> refreshRoutes() { publisher.publishEvent(new RefreshRoutesEvent(this)); return Mono.just("Routes refreshed successfully"); } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } }

Monitoring and Metrics

Example 8: Custom Gateway Metrics

package com.example.apigateway.metrics; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.time.Duration; import java.time.Instant; @Component public class MetricsFilter implements GlobalFilter, Ordered { private final MeterRegistry meterRegistry; public MetricsFilter(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { Instant startTime = Instant.now(); String path = exchange.getRequest().getPath().value(); String method = exchange.getRequest().getMethod().name(); return chain.filter(exchange).doOnSuccessOrError((result, throwable) -> { Instant endTime = Instant.now(); Duration duration = Duration.between(startTime, endTime); // Record request duration meterRegistry.timer("gateway.requests.duration", "path", path, "method", method, "status", String.valueOf(exchange.getResponse().getStatusCode().value())) .record(duration); // Count requests meterRegistry.counter("gateway.requests.total", "path", path, "method", method, "status", String.valueOf(exchange.getResponse().getStatusCode().value())) .increment(); }); } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } }

Testing

Example 9: Gateway Test Configuration

package com.example.apigateway.test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static com.github.tomakehurst.wiremock.client.WireMock.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureWireMock(port = 0) class ApiGatewayTest { @Autowired private WebTestClient webClient; @BeforeEach void setUp() { // Stub for user service stubFor(get(urlEqualTo("/api/users/1")) .willReturn(aResponse() .withHeader("Content-Type", "application/json") .withBody("{\"id\":1,\"name\":\"John Doe\"}"))); } @Test void testUserServiceRoute() { webClient.get().uri("/api/users/1") .exchange() .expectStatus().isOk() .expectBody() .jsonPath("$.id").isEqualTo(1) .jsonPath("$.name").isEqualTo("John Doe"); } @Test void testAuthenticationFilter() { webClient.get().uri("/api/orders/1") .exchange() .expectStatus().isUnauthorized(); } @Test void testRateLimiting() { for (int i = 0; i < 15; i++) { webClient.get().uri("/api/products/1") .exchange(); } webClient.get().uri("/api/products/1") .exchange() .expectStatus().isEqualTo(429); // Too Many Requests } }

Advanced Configuration

Example 10: Advanced Gateway Configuration

# application-advanced.yml spring: cloud: gateway: httpclient: connect-timeout: 1000 response-timeout: 5s pool: type: elastic max-connections: 1000 acquire-timeout: 45000 metrics: enabled: true default-filters: - name: RequestHeader args: name: X-Gateway-Request value: true - name: AddResponseHeader args: name: X-Gateway-Response value: processed management: metrics: enable: gateway: true endpoint: gateway: enabled: true metrics: enabled: true logging: level: org.springframework.cloud.gateway: DEBUG reactor.netty: DEBUG resilience4j: timelimiter: instances: userService: timeout-duration: 3s retry: instances: userService: max-attempts: 3 wait-duration: 500ms

Best Practices

  1. Use Service Discovery: Integrate with Eureka or Consul for dynamic routing
  2. Implement Circuit Breakers: Use Resilience4j for fault tolerance
  3. Rate Limiting: Protect backend services from excessive traffic
  4. Centralized Security: Handle authentication and authorization at gateway level
  5. Request/Response Logging: Log important request/response data
  6. CORS Configuration: Handle cross-origin requests at gateway level
  7. Monitoring: Use Spring Boot Actuator and Micrometer for monitoring
  8. Configuration Management: Use Spring Cloud Config for centralized configuration

Conclusion

Spring Cloud Gateway provides a powerful, flexible way to build API Gateways in Java. Key benefits include:

  • Performance: Built on WebFlux for non-blocking I/O
  • Flexibility: Custom filters and predicates
  • Integration: Seamless Spring ecosystem integration
  • Resilience: Built-in circuit breaker and retry patterns
  • Security: Comprehensive security features
  • Monitoring: Excellent observability support

This setup provides a robust foundation for building microservices architectures with proper API gateway patterns for routing, security, and resilience.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper