Appearance
API Gateway: Filters and Authentication
Chapter 10 used the gateway for routing and load balancing. Everything else on the list (authentication, rate limiting, retries, circuit breakers, transformations, logging) is done with filters.
A filter intercepts a request and response as they pass through the gateway, and can inspect or modify them. There are two kinds:
- Global filters apply to every request through the gateway. Good for authentication and logging.
- Route-specific filters (also called gateway filters) apply only to requests matching one route. Good for rate limiting, retries and circuit breakers, which usually need different settings per service.
The life of a request
A request to localhost:8083/products/1 travels like this:
- The
DispatcherHandler(WebFlux's equivalent of theDispatcherServlet) receives the request. RoutePredicateHandlerMappingchecks each route's predicates and picks the route (/products/**→ route 0).- The request passes through a chain of filters. Each filter runs its pre-logic, then calls the next filter.
RouteToRequestUrlFiltercombines the route's URI with the request path. Forlb://URIs,ReactiveLoadBalancerClientFilterthen asks service discovery for the instances and picks one.NettyRoutingFiltersends the request to the service.- On the way back, the response passes through the filters in reverse order, each running its post-logic.
NettyWriteResponseFilterwrites the response to the client.
Filter order
Some global filters run before the route filters and some after. That is because every filter, global or route-specific, has an order value, and the chain is sorted by it:
- Lower value = higher priority = earlier in the chain (pre-logic first, post-logic last).
- Most global filters have low values, so they run before route filters.
RouteToRequestUrlFilter(10000),ReactiveLoadBalancerClientFilter(10150) andNettyRoutingFilter(Ordered.LOWEST_PRECEDENCE) have very high values on purpose: they must run last, after every other filter has had its say.NettyWriteResponseFilterhas order -1, so it runs early. Its pre-logic is empty; all its work is post-logic, and because it is early in the chain, its post-logic runs last, which is exactly when the response should be written.
Defaults:
- A global filter without an order gets
Ordered.LOWEST_PRECEDENCE. - Route filters without an order get 1, 2, 3... in the order they are listed for the route.
Step 1: a global authentication filter
Authentication applies to every request, so it is a global filter. Implement GlobalFilter, plus Ordered to run it early:
java
@Component
public class JwtAuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// tokens are issued by an auth service; don't demand one to get one
if (path.startsWith("/auth")) {
return chain.filter(exchange);
}
String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); // stop here
}
String token = authHeader.substring(7);
try {
// verify the JWT: signature, expiry, claims...
} catch (Exception e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// pre-logic done: pass the request on, then run post-logic when the response is back
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// post-logic here
}));
}
@Override
public int getOrder() {
return -1; // early in the chain
}
}exchangeholds the request and the response.chain.filter(exchange)passes the request to the next filter.- To reject, set the status and call
setComplete()without calling the chain. The request never reaches a service. - The gateway is reactive:
chain.filter(...)returns at once with aMonothat completes later, when the response is back. Code written after it would run before the downstream call finishes. Post-logic therefore goes inside.then(Mono.fromRunnable(...)), which runs when theMonocompletes.
The gateway only verifies tokens. Issuing them is the job of an authentication service, which is why /auth paths pass through. JWT creation and verification themselves are covered in the Spring Boot book's security chapters.
Calls through the gateway:
text
no Authorization header -> 401 Unauthorized
Authorization: Bearer <bad> -> 401 Unauthorized
Authorization: Bearer <valid> -> 200, forwarded to product-serviceEvery service behind the gateway is now protected without a line of security code in it.
Step 2: built-in route filters
Spring Cloud Gateway ships many route filters, including:
| Filter | Does |
|---|---|
AddRequestHeader | adds a header to the request |
AddResponseHeader | adds a header to the response |
RemoveRequestHeader | removes a header from the request |
RemoveResponseHeader | removes a header from the response |
Retry | retries the request on failure |
RequestRateLimiter | rate limiting |
CircuitBreaker | circuit breaker with fallback |
Filters are a list under each route, configured by name and args:
properties
spring.cloud.gateway.routes[0].id=product-service
spring.cloud.gateway.routes[0].uri=lb://product-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/products/**
# add a request header
spring.cloud.gateway.routes[0].filters[0].name=AddRequestHeader
spring.cloud.gateway.routes[0].filters[0].args.name=X-Test-RequestHeader
spring.cloud.gateway.routes[0].filters[0].args.value=ApiGatewayRequest
# add a response header
spring.cloud.gateway.routes[0].filters[1].name=AddResponseHeader
spring.cloud.gateway.routes[0].filters[1].args.name=X-Test-ResponseHeader
spring.cloud.gateway.routes[0].filters[1].args.value=ApiGatewayResponse
# strip a header clients must not send on
spring.cloud.gateway.routes[0].filters[2].name=RemoveRequestHeader
spring.cloud.gateway.routes[0].filters[2].args.name=Api-private-KeyA client sends Api-private-Key: secret. product-service receives X-Test-RequestHeader: ApiGatewayRequest and no Api-private-Key; the client's response carries X-Test-ResponseHeader: ApiGatewayResponse.
How the names map to classes
The name is a class name minus its suffix: AddRequestHeader is AddRequestHeaderGatewayFilterFactory. Each factory has a config class, and args.<field> sets that class's fields. AddRequestHeaderGatewayFilterFactory uses NameValueConfig, with fields name and value; RemoveRequestHeaderGatewayFilterFactory uses NameConfig, with just name. To learn a filter's arguments, open its factory and read its config class.
Step 3: the Retry filter
properties
# retry GET and DELETE requests up to 4 times
spring.cloud.gateway.routes[0].filters[0].name=Retry
spring.cloud.gateway.routes[0].filters[0].args.retries=4
spring.cloud.gateway.routes[0].filters[0].args.methods=GET,DELETERetryGatewayFilterFactory uses RetryConfig, with fields such as retries, methods, statuses and series.
retriescounts retries, not attempts:retries=4means the original call plus 4 retries, 5 attempts. Against an endpoint that always returns 500, product-service logged five GET attempts.- By default it retries 5xx responses (and I/O failures) for GET only;
methodswidens or narrows that. A POST to the same failing endpoint was tried once: POST is not idempotent (chapter 8).
This filter is the gateway's own implementation, not Resilience4j.
Step 4: the CircuitBreaker filter
This one uses Resilience4j through Spring Cloud CircuitBreaker. Add to the gateway:
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>Configure the filter and the breaker:
properties
spring.cloud.gateway.routes[0].filters[0].name=CircuitBreaker
spring.cloud.gateway.routes[0].filters[0].args.name=myCircuitBreakerName
spring.cloud.gateway.routes[0].filters[0].args.fallbackUri=forward:/fallback
resilience4j.circuitbreaker.instances.myCircuitBreakerName.sliding-window-type=COUNT_BASED
resilience4j.circuitbreaker.instances.myCircuitBreakerName.sliding-window-size=10
resilience4j.circuitbreaker.instances.myCircuitBreakerName.minimum-number-of-calls=5
resilience4j.circuitbreaker.instances.myCircuitBreakerName.failure-rate-threshold=50
resilience4j.circuitbreaker.instances.myCircuitBreakerName.wait-duration-in-open-state=10s
resilience4j.circuitbreaker.instances.myCircuitBreakerName.permitted-number-of-calls-in-half-open-state=3args.namenames the breaker; its settings come from theresilience4j.circuitbreaker.instances.<name>properties exactly as in chapter 9.args.fallbackUri=forward:/fallbacksends failed calls to an endpoint inside the gateway (forward:means an internal forward, not a call to another service):
java
@RestController
public class FallbackController {
@RequestMapping("/fallback")
public ResponseEntity<String> fallback() {
return ResponseEntity.ok("Service is temporarily unavailable, please try again later.");
}
}With product-service stopped, every call returns the fallback message. After the fifth failure the log shows CircuitBreaker 'myCircuitBreakerName' changed state from CLOSED to OPEN, and from then on the gateway does not even try to connect until the wait ends.
RequestRateLimiter works the same way (name plus args); its standard implementation keeps its counters in Redis, so that all gateway instances share them.
Step 5: a custom route filter
Extend AbstractGatewayFilterFactory with your own config class. The class name must end in GatewayFilterFactory:
java
@Component
public class CustomRouteGatewayFilterFactory
extends AbstractGatewayFilterFactory<CustomRouteGatewayFilterFactory.CustomConfig> {
public CustomRouteGatewayFilterFactory() {
super(CustomConfig.class);
}
@Override
public GatewayFilter apply(CustomConfig config) {
return (exchange, chain) -> {
System.out.println("pre filter logic, country: " + config.getCountry());
return chain.filter(exchange).then(Mono.fromRunnable(() ->
System.out.println("post filter logic")));
};
}
public static class CustomConfig {
private String country;
public String getCountry() { return country; }
public void setCountry(String country) { this.country = country; }
}
}Use it by name (CustomRoute), with country as an arg. Give it the next free index in the route's filter list:
properties
spring.cloud.gateway.routes[0].filters[3].name=CustomRoute
spring.cloud.gateway.routes[0].filters[3].args.country=IndiaWith the global JWT filter (order -1) and this route filter both active, one request logs:
text
global pre
route pre, country=India
route post
global postThe global filter's pre-logic runs first and its post-logic last, wrapped around the route filter: the ordering rules above, in action.
Next: The services are reachable, protected and resilient, but each still carries its own application.properties. Chapter 12 moves configuration out of the services into a central config server.
Check yourself
- What is the difference between a global filter and a route-specific filter? Give an example use of each.
- Trace a request through the gateway, from
DispatcherHandlerto the response. - How is the filter chain ordered, and what does a lower order value mean?
- Why do
NettyRoutingFilterandRouteToRequestUrlFilterhave very high order values? - Why does
NettyWriteResponseFilterhave order -1? - What order do route filters get by default?
- How does a global filter reject a request?
- Why must post-logic go in
.then(Mono.fromRunnable(...))? - Why does the JWT filter let
/authpaths through? - How does
filters[0].name=AddRequestHeaderwithargs.nameandargs.valuemap to code? - How many attempts does
Retrywithargs.retries=4make, and is a POST retried by default? - What does
fallbackUri=forward:/fallbackdo? - What must a custom filter class be named, and how do you pass it settings?
Answers
- A global filter applies to every request (authentication, logging); a route filter applies only to requests on one route (retry, circuit breaker or rate limiting with per-service settings).
DispatcherHandler→RoutePredicateHandlerMappingpicks the route → filters' pre-logic in order →RouteToRequestUrlFilterbuilds the URL,ReactiveLoadBalancerClientFilterresolveslb://→NettyRoutingFiltercalls the service → filters' post-logic in reverse order →NettyWriteResponseFilterwrites the response.- By each filter's order value, global and route filters together; a lower value means higher priority: earlier pre-logic and later post-logic.
- They must run after all other filters: they build the final URL and send the request.
- So it is early in the chain and its post-logic, which writes the response to the client, runs last.
- 1, 2, 3... in the order they are listed for the route.
- Set the response status (e.g. 401) and return
exchange.getResponse().setComplete()without calling the chain. - The gateway is reactive:
chain.filterreturns immediately and completes later; code after it would run before the response arrives. - Tokens are issued there; requiring a token to get a token would lock everyone out.
- The name selects
AddRequestHeaderGatewayFilterFactory;args.nameandargs.valueset the fields of its config class (NameValueConfig). - Five (the call plus 4 retries); no, only GET by default.
- When the call fails or the circuit is open, the gateway forwards the request internally to its own
/fallbackendpoint. <Name>GatewayFilterFactory, extendingAbstractGatewayFilterFactorywith a config class; settings go infilters[i].args.<field>, used asname=<Name>.