Spring Cloud Gateway Filter ordering - filter

I'm using spring cloud gateway filter, and want to filter request by GatewayFilterFactory,
but order not working with #Order annotation
I tried to swap the order of filters, but not working.
#Slf4j
#Order(1)
#Component
public class AccessFilter implements GatewayFilterFactory<AccessFilter.Config> {
#Override
public GatewayFilter apply(AccessFilter.Config config) {
return (exchange, chain) -> {
log.info("in AccessFilter second");
// do something
}
}
}
#Order(-1)
#Slf4j
#Component
public class AddTraceIdGlobalFilter implements GlobalFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("in globalfilter first");
// do something
}
}
My yaml file:
spring:
cloud:
gateway:
routes:
- id: user_1
uri: http://user:8080
predicates:
- Path=/v1/user/info
filters:
- AccessFilter
I expect the output:
in globalfilter first
in AccessFilter second
but the actual output:
in AccessFilter second
in globalfilter first

GatewayFilterFactory cannot be applied #Order or Ordered.
You have to use OrderedGatewayFilter.
#Component
public class AccessFilter implements GatewayFilterFactory<AccessFilter.Config> {
#Override
public GatewayFilter apply(AccessFilter.Config config) {
return new OrderedGatewayFilter((exchange, chain) -> {
log.info("in AccessFilter second");
// do something
}, 1)
}
}

You can implement Ordered interface:
public class RewriteTokenGlobalPreFilter implements GlobalFilter, Ordered
{
....
#Override
public int getOrder() {
return 3;
}
}

Related

How can I forward request using HandlerFilterFunction?

A server environment requires an endpoint for /some/health.
I already configured actuator.
Rather changing the actuator's function, I'm thinking forwarding /some/health to the /actuator/health.
And I'm trying to do with HandlerFilterFunction.
#Configuration
public class SomeHealthFilterFunction
implements HandlerFilterFunction<ServerResponse, ServerResponse> {
private static final String PATTERN = "/some/health";
#Override
public Mono<ServerResponse> filter(ServerRequest request,
HandlerFunction<ServerResponse> next) {
if (PATTERN.equals(request.requestPath().pathWithinApplication().value())) {
RequestPath requestPath
= request.requestPath().modifyContextPath("/actuator/health");
// How can I call next.handle, here?
}
}
}
How can I change the origin request and do next.handle(...)?
Here's an example WebFilter that will reroute all calls from /some/health to /actuator/health
#Component
public class RerouteWebFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
ServerHttpRequest request = serverWebExchange.getRequest();
if ("/some/health".equals(request.getPath().pathWithinApplication().value())) {
ServerHttpRequest mutatedServerRequest = request.mutate().path("/actuator/health").build();
serverWebExchange = serverWebExchange.mutate().request(mutatedServerRequest).build();
}
return webFilterChain.filter(serverWebExchange);
}
}

How do I spring cloud gateway custom filter e2e test?

I have implemented custom GatewayFilterFactory filter. But I don't know how to test this filter with e2e setup.
I have referenced official spring-cloud-gateway AddRequestHeaderGatewayFilterFactoryTests test case code.
This is my custom filter code:
#Component
public class MyCustomFilter implements GatewayFilterFactory<MyCustomFilter.Config>, Ordered {
#Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter((this::filter), getOrder());
}
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
/* do some filtering */
}
#Override
public int getOrder() {
return 1000;
}
#Override
public Config newConfig() {
return new Config(MyCustomFilter.class.getSimpleName());
}
public static getConfig() {
return
}
#Getter
#Setter
public static class Config {
private String name;
Config(String name) {
this.name = name;
}
}
}
And this is my test code:
BaseWebClientTests class look exactly the same as official BaseWebClientTests class code
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = RANDOM_PORT)
#DirtiesContext
#ActiveProfiles("my-custom-filter")
public class MyCustomFilterTests extends BaseWebClientTests {
#LocalServerPort
protected int port = 0;
protected WebTestClient testClient;
protected WebClient webClient;
protected String baseUri;
#Before
public void setup() throws Exception {
setup(new ReactorClientHttpConnector(), "http://localhost:" + port);
}
protected void setup(ClientHttpConnector httpConnector, String baseUri) {
this.baseUri = baseUri;
this.webClient = WebClient.builder().clientConnector(httpConnector)
.baseUrl(this.baseUri).build();
this.testClient = WebTestClient
.bindToServer(httpConnector)
.baseUrl(this.baseUri)
.build();
}
#Test
public void shouldFailByFilterTests() {
/* This test should be failed but success :( */
testClient.get().uri("/api/path")
.exchange().expectBody(Map.class).consumeWith(result -> {
/* do assertion */
});
}
#EnableAutoConfiguration
#SpringBootConfiguration
#Import(DefaultTestConfig.class)
public static class TestConfig {
#Value("${test.uri}")
String uri;
#Bean
public MyCustomFilter myCustomFilter() {
return new MyCustomFilter();
}
#Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder, MyCustomFilter myCustomFilter) {
return builder.routes().route("my_custom_filter",
r -> r.path("/api/path")
.filters(f -> f.filter(myCustomFilter.apply(new MyCustomFilter.Config("STRING"))))
.uri(uri))
.build();
}
}
}
Lastly Target controller looks like this:
#RestController
#RequestMapping("/api/path")
public class HttpBinCompatibleController {
#GetMapping("/")
public Mono<BodyData> identity() {
return Mono.just(new BodyData("api success"));
}
#NoArgsConstructor
#AllArgsConstructor
#Getter
static class BodyData {
private String message;
}
}
What I understand how this filter factory test code works is that
custom filter: custom filter is setup inside TestConfig class testRouteLocator method
target controller: target controller is defined as HttpBinCompatibleController class
testClient sends the request, and custom should do some filtering, then target controller should receive the request from testClient.
What I expect from this shouldFailByFilterTests TC is that before request from testClient is sent to target controller, that request should be rejected by MyCustomFilter. But the request is sent to the target controller.
I think the request from testClient is not proxied by testRouteLocator but I'm not sure
Question
What is the cause of this problem?
Is there another way to test my own custom filter?
This problem was related to the version incompatibility between Spring Boot and Spring Cloud.
I was using Spring Boot version 2.1.7 and Spring Cloud version Greenwich.SR2.
Then I found this 'Release train Spring Boot compatibility' table on this link
Before I've noticed version incompatibility, for using #Configuration(proxyBeanMethods = false) feature, upgraded Spring Boot version to 2.2.x.
The solution is using 2.1.x branch BaseWebClientTests class.

Spring Cloud Gateway with Spring Security

I have been using spring cloud gateway along with spring security to protect my actuator endpoints and a custom gateway filter for authorization filtering.
Below are the implementations I have made :
For Spring security I have used the following config :
#EnableWebFluxSecurity
public class WebSecurityConfig {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.csrf().disable().authorizeExchange().pathMatchers("/actuator/**").authenticated().anyExchange().permitAll().and().httpBasic();
return http.build();
}
}
And my custom filter is like this:
#Component
public class AuthorizationRequestFilter implements GatewayFilterFactory<AuthorizationRequestFilter.Config>, Ordered {
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String authToken = request.getHeaders().getFirst("Authorization");
if (StringUtils.isEmpty(authToken)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
byte[] response = "{\"status\":\"401\",\"message\":\"Unauthorized.\"}".getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(response);
return exchange.getResponse().writeWith(Flux.just(buffer));
}
return chain.filter(exchange.mutate().request(request).build());
};
}
#Override
public Config newConfig() {
return new Config("AuthorizationRequestFilter");
}
public static class Config {
public Config(String name) {
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
#Override
public int getOrder() {
return 1;
}
}
Is there any way I can achieve this in a single place where I can configure both?

How to add a custom header in Spring WebFilter?

I'm trying to add a custom filter before I invoke my REST Service. In this below class, I'm trying to add the custom filter in the HttpRequest but I'm getting error :-
java.lang.UnsupportedOperationException: null
at java.util.Collections$UnmodifiableMap.computeIfAbsent(Collections.java:1535) ~[na:1.8.0_171]
at org.springframework.util.CollectionUtils$MultiValueMapAdapter.add(CollectionUtils.java:459) ~[spring-core-5.0.7.RELEASE.jar:5.0.7.RELEASE]
public class AuthenticationWebFilter implements WebFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationWebFilter.class);
#Autowired
private TokenServiceRequest tokenServiceRequest;
#Autowired
private AuthenticationProvider authenticationProvider;
public AuthenticationWebFilter(TokenServiceRequest tokenServiceRequest, AuthenticationProvider authenticationProvider) {
super();
this.tokenServiceRequest = tokenServiceRequest;
this.authenticationProvider = authenticationProvider;
}
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
HttpHeaders requestHeaders = serverWebExchange.getRequest().getHeaders();
HttpHeaders responseHeaders = serverWebExchange.getResponse().getHeaders();
LOGGER.info("Response HEADERS: "+responseHeaders);
LOGGER.info("Request HEADERS: "+serverWebExchange.getRequest().getHeaders());
tokenServiceRequest.setUsername(serverWebExchange.getRequest().getHeaders().getFirst(CommerceConnectorConstants.USERNAME));
tokenServiceRequest.setPassword(serverWebExchange.getRequest().getHeaders().getFirst(CommerceConnectorConstants.PASSWORD));
tokenServiceRequest.setClientId(serverWebExchange.getRequest().getHeaders().getFirst(CommerceConnectorConstants.CLIENT_ID));
tokenServiceRequest.setSecretClient(serverWebExchange.getRequest().getHeaders().getFirst(CommerceConnectorConstants.SECRET_CLIENT));
LOGGER.info("Token Received: " + authenticationProvider.getUserAccessToken(tokenServiceRequest).getTokenId());
//responseHeaders.set(CommerceConnectorConstants.X_AUTH_TOKEN, authenticationProvider.getUserAccessToken(tokenServiceRequest).getTokenId());
//responseHeaders.add(CommerceConnectorConstants.X_AUTH_TOKEN, authenticationProvider.getUserAccessToken(tokenServiceRequest).getTokenId());
//This below code is not working
serverWebExchange.getRequest().getQueryParams().add("test", "value");
//This below code is not working
//serverWebExchange.getRequest().getHeaders().add(CommerceConnectorConstants.X_AUTH_TOKEN, authenticationProvider.getUserAccessToken(tokenServiceRequest).getTokenId());
LOGGER.info("Exiting filter#AuthenticationWebFilter");
return webFilterChain.filter(serverWebExchange);
}
}
In HTTPResponse, I can set the custom headers but my requirement is to add the custom header in the HTTPRequest. Please advise.
If you're in spring cloud gateway, request header could be modified by implements GlobalFilter or GatewayFilter.
#Component
public class LogFilter implements GlobalFilter, Ordered {
private Logger LOG = LoggerFactory.getLogger(LogFilter.class);
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(
exchange.mutate().request(
exchange.getRequest().mutate()
.header("customer-header", "customer-header-value")
.build())
.build());
}
#Override
public int getOrder() {
return 0;
} }
If you're in ZuulFilter, addZuulRequestHeader could modified the request header.
RequestContext.getCurrentContext().addZuulRequestHeader("customer-header", "customer-header-value");
Hope it's helpful.
public class CustomTokenFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
ServerHttpRequest mutateRequest = serverWebExchange.getRequest().mutate()
.header("token", "test")
.build();
ServerWebExchange mutateServerWebExchange = serverWebExchange.mutate().request(mutateRequest).build();
return webFilterChain.filter(mutateServerWebExchange);
}
}
I think the exception is thrown because of security reasons. It would be nasty if a filter could add/modify the HTTP request headers. Of course, you can accomplish this by creating a series of decorators:
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
public class CustomFilter implements WebFilter {
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
ServerWebExchangeDecorator decorator = new ServerWebExchangeDecoratorImpl(serverWebExchange);
//do your stuff using decorator
return webFilterChain.filter(decorator);
}
}
class ServerWebExchangeDecoratorImpl extends ServerWebExchangeDecorator {
private ServerHttpRequestDecorator requestDecorator;
public ServerWebExchangeDecoratorImpl(ServerWebExchange delegate) {
super(delegate);
this.requestDecorator = new ServerHttpRequestDecoratorImpl(delegate.getRequest());
}
#Override
public ServerHttpRequest getRequest() {
return requestDecorator;
}
}
class ServerHttpRequestDecoratorImpl extends ServerHttpRequestDecorator {
// your own query params implementation
private MultiValueMap queryParams;
public ServerHttpRequestDecoratorImpl(ServerHttpRequest request) {
super(request);
this.queryParams = new HttpHeaders();
this.queryParams.addAll(request.getQueryParams());
}
#Override
public MultiValueMap<String, String> getQueryParams() {
return queryParams;
}
//override other methods if you want to modify the behavior
}
I'm having the same problem because headers already have the same key; My solution is to set the key in the header, first check whether the key exists;
#Configuration
public class AuthGatewayFilter implements GlobalFilter, Ordered {
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Consumer<HttpHeaders> httpHeaders = httpHeader -> {
// check exists
if(StringUtils.isBlank(httpHeader.getFirst("xxx"))){
httpHeader.add("xxx", "xxx");
}
};
ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate().headers(httpHeaders).build();
exchange = exchange.mutate().request(serverHttpRequest).build();
return chain.filter(exchange);
}
}

Custom AbstractEndpoint listening to "/" (root)

I've implemented a starter that configures Swagger the way I like. In addition, I'd like to redirect every call to the app's root URL (e.g. localhost:8080) to /swagger-ui.html.
Therefore, I added an own AbstractEndpoint which is instantiated in the #Configuration class as follows:
#Configuration
#Profile("swagger")
#EnableSwagger2
public class SwaggerConfig {
...
#Bean
public RootEndpoint rootEndpoint() {
return new RootEndpoint();
}
#Bean
#ConditionalOnBean(RootEndpoint.class)
#ConditionalOnEnabledEndpoint("root")
public RootMvcEndpoint rootMvcEndpoint(RootEndpoint rootEndpoint) {
return new RootMvcEndpoint(rootEndpoint);
}
}
The respective classes look like this:
public class RootEndpoint extends AbstractEndpoint<String> {
public RootEndpoint() {
super("root");
}
#Override
public String invoke() {
return ""; // real calls shall be handled by RootMvcEndpoint
}
}
and
public class RootMvcEndpoint extends EndpointMvcAdapter {
public RootMvcEndpoint(RootEndpoint delegate) {
super(delegate);
}
#RequestMapping(method = {RequestMethod.GET}, produces = { "*/*" })
public void redirect(HttpServletResponse httpServletResponse) throws IOException {
httpServletResponse.sendRedirect("/swagger-ui.html");
}
}
As stated in public RootEndpoint(), the custom Endpoint is bound to /root. Unfortunately, I can't specify super(""); or super("/"); as those values throw an exception (Id must only contains letters, numbers and '_').
How can I achieve having a custom Endpoint listening to the root URL in a starter using #Configuration files to instantiate beans?
I solved it with an easier approach by adding a WebMvcConfigurerAdapter bean in the #Configuration:
#Bean
public WebMvcConfigurerAdapter redirectToSwagger() {
return new WebMvcConfigurerAdapter() {
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("").setViewName("redirect:/swagger-ui.html");
}
};
}

Resources