How can I forward request using HandlerFilterFunction? - spring

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);
}
}

Related

Why isn't my Spring WebFilter being applied to my webclient API requests?

I am trying to create a WebFilter for my Spring App's web client requests so that a token will be added within the headers of the request. It seems that my WebFilter isn't ever called since the println I've added in the filter is never printed out.
This is my WebFilter
#Component
public class Auth0RequestFilter implements WebFilter {
#Value("${auth0.audiences}")
private Set<String> auth0Audiences;
#Autowired
Auth0Client auth0Client;
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange,
WebFilterChain webFilterChain) {
String audience = serverWebExchange.getRequest().getURI().getHost();
System.out.println("We've reached this piece of code");
if(auth0Audiences.contains(audience)) {
String accessToken = auth0Client.getAccessToken(audience);
serverWebExchange.getRequest().getHeaders().add("authorization", "Bearer ".concat(accessToken));
}
return webFilterChain.filter(serverWebExchange);
}
}
And this is my API request:
#Component
public class TestAPICall {
final Auth0RequestFilter auth0RequestFilter;
public TestAPICall(Auth0RequestFilter auth0RequestFilter) {
this.auth0RequestFilter = auth0RequestFilter;
}
WebClient client = WebClient.create();
#Scheduled(fixedRate = 10000)
public void scheduleFixedRateTask() {
client.get()
.uri("https://google.com")
.retrieve()
.bodyToMono(String.class)
.block();
}
}
WebFilter is a contract for interception-style, chained processing of server Web requests, not client.
To intercept client requests you need to use ExchangeFilterFunction
ExchangeFilterFunction filterFunction = (clientRequest, nextFilter) -> {
…
return nextFilter.exchange(clientRequest);
};
and then add it to the WebClient instance
WebClient webClient = WebClient.builder()
.filter(filterFunction)
.build();

Spring Cloud gateway send response in filter

I am using spring cloud gateway as edge server.
This is the flow
If request has a header named 'x-foo' then find the header value, get a string from another server and send that string as response instead of actually proxying the request.
Here is code for Filter DSL
#Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("foo-filter", r -> r.header('x-foo').and().header("x-intercepted").negate()
.filters(f -> f.filter(fooFilter))
.uri("http://localhost:8081")) // 8081 is self port, there are other proxy related configurations too
.build();
}
Code for Foo filter
#Component
#Slf4j
public class FooFilter implements GatewayFilter {
#Autowired
private ReactiveRedisOperations<String, String> redisOps;
#Value("${header-name}")
private String headerName;
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
var foo = request.getHeaders().getFirst(headerName);
return redisOps.opsForHash()
.get("foo:" + foo, "response")
.doOnSuccess(s -> {
log.info("data on success");
log.info(s.toString()); // I am getting proper response here
if (s != null) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().set("x-intercepted", "true");
byte[] bytes = s.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
response.writeWith(Mono.just(buffer));
response.setComplete();
}
})
.then(chain.filter(exchange));
}
}
The problem is, the response has the response is getting proper 200 code, the injected header is present on response but the data is not available in response.
This is how I got working.
Use flatMap instead of doOnSuccess
don't use then or switchIfEmpty instead use onErrorResume
Return the response.writeWith
#Component
#Slf4j
public class FooFilter implements GatewayFilter {
#Autowired
private ReactiveRedisOperations<String, String> redisOps;
#Value("${header-name}")
private String headerName;
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
var foo = request.getHeaders().getFirst(headerName);
return redisOps.opsForHash()
.get("foo:" + foo, "response")
.flatMap(s -> {
log.info("data on success");
log.info(s.toString()); // I am getting proper response here
if (s != null) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().set("x-intercepted", "true");
byte[] bytes = s.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}else{ return chain.filter(exchange).then(Mono.fromRunnable(() -> {log.info("It was empty")} }
})
.onErrorResume(chain.filter(exchange));
}
}

Spring, webflux: The getRemoteAddress method of the ServerHttpRequest object returns null when request performed from WebTestClient

I have a controller
#RestController
public class NameController {
#Autowired
private NameService nameService;
#GetMapping("/name")
public Mono<UploadParamsDto> getName(ServerHttpRequest request) {
return nameService.getNameByHost(request.getRemoteAddress().getHostName());
}
}
and i have a test method:
#ExtendWith(SpringExtension.class)
#WebFluxTest(NameControllerTest.class)
#ActiveProfiles("test")
class NameControllerTest {
#Autowired
private WebTestClient webClient;
#Test
void nameTest() {
webClient.get().uri("/name")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isOk();
}
}
When I run the test in order to check my getName method i got NPE because
request.getRemoteAddress() returns null, could you please tell me how to mock ServerHttpRequest? (I know that there is MockServerHttpRequest, but I couldn't managed with it)
My purpose is make request.getRemoteAddress().getHostName() return mock value.
Thanks to everyone.
Works in next way:
#ExtendWith(SpringExtension.class)
#WebFluxTest(NameControllerTest.class)
#ActiveProfiles("test")
class NameControllerTest {
#Autowired
private ApplicationContext context;
#Test
void nameTest() {
WebTestClient webClient = WebTestClient
.bindToApplicationContext(context)
.webFilter(new SetRemoteAddressWebFilter("127.0.0.1"))
.configureClient()
.build();
webClient.get().uri("/name")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isOk();
}
}
Where SetRemoteAddressWebFilter is WebFilter:
public class SetRemoteAddressWebFilter implements WebFilter {
private String host;
public SetRemoteAddressWebFilter(String host) {
this.host = host;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(decorate(exchange));
}
private ServerWebExchange decorate(ServerWebExchange exchange) {
final ServerHttpRequest decorated = new ServerHttpRequestDecorator(exchange.getRequest()) {
#Override
public InetSocketAddress getRemoteAddress() {
return new InetSocketAddress(host, 80);
}
};
return new ServerWebExchangeDecorator(exchange) {
#Override
public ServerHttpRequest getRequest() {
return decorated;
}
};
}
}
Running a test with #WebFluxTest doesn't involve a real server, you've figured that out.
But getting a NullPointerException doesn't feel right still - could you create an issue on https://jira.spring.io about that? I don't think you should have to work around this, but Spring Framework should probably provide some infrastructure to "mock" that information.

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);
}
}

How to intercept requests by handler method in Spring WebFlux

I've got following interceptor in Spring MVC that checks if user can access handler method:
class AccessInterceptor : HandlerInterceptorAdapter() {
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any?): Boolean {
val auth: Auth =
(if (method.getAnnotation(Auth::class.java) != null) {
method.getAnnotation(Auth::class.java)
} else {
method.declaringClass.getAnnotation(Auth::class.java)
}) ?: return true
if (auth.value == AuthType.ALLOW) {
return true
}
val user = getUserFromRequest(request) // checks request for auth token
// and checking auth for out user in future.
return renderError(403, response)
In my Controller I do annotate methods, like this:
#GetMapping("/foo")
#Auth(AuthType.ALLOW)
fun doesntNeedAuth(...) { ... }
#GetMapping("/bar")
#Auth(AuthType.ADMIN)
fun adminMethod(...) { ... }
In case if user has wrong token or no permissions, error is being returned.
Is it possible to do this in Spring WebFlux with annotation-style controllers?
My implementation, w/o using toFuture().get() which is potentially blocking.
#Component
#ConditionalOnWebApplication(type = Type.REACTIVE)
public class QueryParameterValidationFilter implements WebFilter {
#Autowired
private RequestMappingHandlerMapping handlerMapping;
#NonNull
#Override
public Mono<Void> filter(#NonNull ServerWebExchange exchange, #NonNull WebFilterChain chain) {
return handlerMapping.getHandler(exchange)
.doOnNext(handler -> validateParameters(handler, exchange))
.then(chain.filter(exchange));
}
private void validateParameters(Object handler, ServerWebExchange exchange) {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Set<String> expectedQueryParams = Arrays.stream(handlerMethod.getMethodParameters())
.map(param -> param.getParameterAnnotation(RequestParam.class))
.filter(Objects::nonNull)
.map(RequestParam::name)
.collect(Collectors.toSet());
Set<String> actualQueryParams = exchange.getRequest().getQueryParams().keySet();
actualQueryParams.forEach(actual -> {
if (!expectedQueryParams.contains(actual)) {
throw new InvalidParameterException(ERR_MSG, actual);
}
});
}
}
}
To solve that problem I would most probably use:
A Spring Reactive Web WebFilter from the WebHandler API to intercept the incoming request
The RequestMappingHandlerMapping to retrieve the method which handles the current request
#Autowired
RequestMappingHandlerMapping requestMappingHandlerMapping;
...
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
...
HandlerMethod handler = (HandlerMethod) requestMappingHandlerMapping.getHandler(exchange).toProcessor().peek();
//your logic
}
#Component
class AuditWebFilter(
private val requestMapping: RequestMappingHandlerMapping
): WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
// if not to call - then exchange.attributes will be empty
// so little early initializate exchange.attributes by calling next line
requestMapping.getHandler(exchange)
val handlerFunction = exchange.attributes.get(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE) as HandlerMethod
val annotationMethod = handlerFunction.method.getAnnotation(MyAnnotation::class.java)
// annotationMethod proccesing here
}
}
In newer versions of Spring the .toProcessor() call is deprecated. What worked for me is to use .toFuture().get() instead:
if(requestMappingHandlerMapping.getHandler(exchange).toFuture().get() instanceof HandlerMethod handlerMethod) { ... }
Unfortunately this requires handling of checked exceptions so the code will be a bit less readable but at least not deprecated anymore.

Resources