如何用Spring Cloud Gateway实现无循环的Sidecar双向流量代理?
Great question! That infinite loop is a classic pitfall when building sidecar proxies—your current setup can't tell apart external inbound requests and the outbound requests coming from your backend app. Let's fix this properly with Spring Cloud Gateway, which is tailor-made for this sidecar scenario.
We'll split the sidecar's responsibilities into two distinct routes, using a custom header to distinguish request types:
- Inbound route: Handles external client requests, validates auth headers, then forwards to your backend app.
- Outbound route: Handles backend app's outgoing requests, adds the required auth header, then forwards to external services.
The custom header will act as a "fingerprint" to prevent loops—backend app adds it to all its outbound requests, so the sidecar knows not to route those back to itself.
Step 1: Configure Your Backend App to Target the Sidecar
First, update your backend app to send all outbound requests to the sidecar, and add a custom header like X-Proxy-Source: backend-app to mark these requests. Here's how to do it for common HTTP clients:
For WebClient:
@Bean public WebClient webClient() { return WebClient.builder() .baseUrl("http://localhost:8081") // Sidecar's port .defaultHeader("X-Proxy-Source", "backend-app") .build(); }
For RestTemplate:
@Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); // Add the custom header to all outgoing requests restTemplate.getInterceptors().add((request, body, execution) -> { request.getHeaders().add("X-Proxy-Source", "backend-app"); return execution.execute(request, body); }); // Route all requests through the sidecar proxy SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost", 8081))); restTemplate.setRequestFactory(factory); return restTemplate; }
Step 2: Build the Sidecar with Spring Cloud Gateway
1. Add Dependencies
Ensure your sidecar's pom.xml includes these core dependencies:
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> </dependencies>
2. Gateway Route Configuration (application.yml)
This config defines the two routes and uses predicates to filter request types:
server: port: 8081 # Sidecar's listening port spring: cloud: gateway: routes: # Route 1: Handle external inbound requests - id: inbound-route uri: http://localhost:8080 # Your backend app's port predicates: - Path=/** - Header=!X-Proxy-Source # Only match requests WITHOUT the custom header filters: - name: RequestAuthValidator # Custom filter to check auth header args: requiredHeader: Authorization - RewritePath=/**, /$1 # Preserve original path when forwarding # Route 2: Handle backend app's outbound requests - id: outbound-route uri: https://default-external-service.com # Placeholder; will be overridden predicates: - Path=/** - Header=X-Proxy-Source, backend-app # Only match requests FROM backend filters: - name: OutboundAuthAdder # Custom filter to add auth header args: authValue: Bearer YOUR_DYNAMIC_OR_STATIC_TOKEN - RemoveRequestHeader=X-Proxy-Source # Clean up before sending to external services - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, .* # Fix redirects
3. Custom Gateway Filters
Implement the two filters to handle auth validation and header addition:
a. RequestAuthValidator (Inbound Validation)
@Component public class RequestAuthValidator implements GatewayFilter, Ordered { private final String requiredHeader; public RequestAuthValidator(@Value("${spring.cloud.gateway.routes[0].filters[0].args.requiredHeader}") String requiredHeader) { this.requiredHeader = requiredHeader; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // Reject requests missing the required auth header if (!exchange.getRequest().getHeaders().containsKey(requiredHeader)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // Optional: Add logic to validate the header's value here (e.g., JWT check) return chain.filter(exchange); } @Override public int getOrder() { return -1; // Run this filter first } }
b. OutboundAuthAdder (Outbound Header Addition)
@Component public class OutboundAuthAdder implements GatewayFilter, Ordered { private final String authValue; public OutboundAuthAdder(@Value("${spring.cloud.gateway.routes[1].filters[0].args.authValue}") String authValue) { this.authValue = authValue; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // Add the required auth header and remove our custom source header ServerHttpRequest modifiedRequest = exchange.getRequest().mutate() .header("Authorization", authValue) .headers(headers -> headers.remove("X-Proxy-Source")) .build(); return chain.filter(exchange.mutate().request(modifiedRequest).build()); } @Override public int getOrder() { return 0; // Run after route matching } }
Step 3: How This Prevents Infinite Loops
The magic is in the route predicates:
- External requests don't have the
X-Proxy-Sourceheader, so they match the inbound route and get forwarded to your backend app. - Backend app's outgoing requests have
X-Proxy-Source: backend-app, so they match the outbound route and get sent to external services—never looping back to the backend.
Bonus: Spring Cloud Netflix Sidecar Alternative
If you want a more out-of-the-box solution, Spring Cloud's Netflix Sidecar module is built specifically for this sidecar pattern. It auto-registers your backend app as a service, handles proxying, and lets you add custom filters for auth logic. The custom Gateway approach gives you more granular control, but Sidecar can save you boilerplate code.
内容的提问来源于stack exchange,提问作者Alstresh

