Spring Cloud gateway send response in filter - spring-boot

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

Related

JwtAuthenticationFilter Junit Testcases

#Component
#Slf4j
public class JwtAuthenticationFilter implements GatewayFilter {
#Autowired
private JwtUtil jwtUtil;
#Override
public Mono<Void> filter(final ServerWebExchange exchange,
final GatewayFilterChain chain) {
log.info("Start --> filter()");
ServerHttpRequest request = (ServerHttpRequest) exchange.getRequest();
if (!request.getHeaders().containsKey("Authorization")) {
ServerHttpResponse response = exchange.getResponse();
log.debug("response status {}", response.getStatusCode());
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
final String token = request.getHeaders().getOrEmpty("Authorization").get(0);request = {ReactorServerHttpRequest#12622}
try {
jwtUtil.validateToken(token);
} catch (JwtTokenMalformedException | JwtTokenMissingException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.BAD_REQUEST);
log.debug("response status {}", response.getStatusCode());
return response.setComplete();
}
Claims claims = jwtUtil.getClaims(token);
exchange.getRequest().mutate().header("id", String.valueOf(claims.get("id"))).build();
log.info("end filter()");
return chain.filter(exchange);
}
}
can someone please explain me how to write junits for this. I am very much new to this Junits and i tried in google also, but could not find the how to check if conditions using Junit/Mockito

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

Spring WebFlux, Security and request body

I need to secure REST API implemented with Spring Boot, WebFlux and spring security using HMAC of the request body. Simplifying a bit, on a high level - request comes with the header that has hashed value of the request body, so I have to read the header, read the body, calculate hash of the body and compare with the header value.
I think I should implement ServerAuthenticationConverter but all examples I was able to find so far only looking at the request headers, not the body and I'm not sure if I could just read the body, or should I wrap/mutate the request with cached body so it could be consumed by the underlying component second time?
Is it ok to use something along the lines of:
public class HttpHmacAuthenticationConverter implements ServerAuthenticationConverter {
#Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
exchange.getRequest().getBody()
.next()
.flatMap(dataBuffer -> {
try {
return Mono.just(StreamUtils.copyToString(dataBuffer.asInputStream(), StandardCharsets.UTF_8));
} catch (IOException e) {
return Mono.error(e);
}
})
...
I'm getting a warning from the IDE on the copyToString line: Inappropriate blocking method call
Any guidelines or examples?
Thanks!
I have also tried:
#Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return Mono.justOrEmpty(exchange.getRequest().getHeaders().toSingleValueMap())
.zipWith(exchange.getRequest().getBody().next()
.flatMap(dataBuffer -> Mono.just(dataBuffer.asByteBuffer().array()))
)
.flatMap(tuple -> create(tuple.getT1(), tuple.getT2()));
But that doesn't work - code in the create() method on the last line is never executed.
I make it work. Posting my code for the reference.
Two components are required to make it work - WebFilter that would read and cache request body so it could be consumed multiple times and the ServerAuthenticationConverter that would calculate hash on a body and validate signature.
public class HttpRequestBodyCachingFilter implements WebFilter {
private static final byte[] EMPTY_BODY = new byte[0];
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// GET and DELETE don't have a body
HttpMethod method = exchange.getRequest().getMethod();
if (method == null || method.matches(HttpMethod.GET.name()) || method.matches(HttpMethod.DELETE.name())) {
return chain.filter(exchange);
}
return DataBufferUtils.join(exchange.getRequest().getBody())
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
})
.defaultIfEmpty(EMPTY_BODY)
.flatMap(bytes -> {
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
#Nonnull
#Override
public Flux<DataBuffer> getBody() {
if (bytes.length > 0) {
DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();
return Flux.just(dataBufferFactory.wrap(bytes));
}
return Flux.empty();
}
};
return chain.filter(exchange.mutate().request(decorator).build());
});
}
}
public class HttpJwsAuthenticationConverter implements ServerAuthenticationConverter {
private static final byte[] EMPTY_BODY = new byte[0];
#Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
})
.defaultIfEmpty(EMPTY_BODY)
.flatMap(body -> create(
exchange.getRequest().getMethod(),
getFullRequestPath(exchange.getRequest()),
exchange.getRequest().getHeaders(),
body)
);
}
...
The create method in the Converter implements the logic to validate signature based on the request method, path, headers and the body. It returns an instance of the Authentication if successful or Mono.empty() if not.
The wiring up is done like this:
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange().pathMatchers(PATH_API).authenticated()
...
.and()
.addFilterBefore(new HttpRequestBodyCachingFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterAt(jwtAuthenticationFilter(...), SecurityWebFiltersOrder.AUTHENTICATION);
}
private AuthenticationWebFilter jwtAuthenticationFilter(ReactiveAuthenticationManager authManager) {
AuthenticationWebFilter authFilter = new AuthenticationWebFilter(authManager);
authFilter.setServerAuthenticationConverter(new HttpJwsAuthenticationConverter());
authFilter.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(PATH_API));
return authFilter;
}
#Bean
public ReactiveAuthenticationManager reactiveAuthenticationManager() {
return Mono::just;
}
}

Spring cloud gateway with Spring cache and caffeine

I have a spring cloud gateway which forwards the API rest requests to some microservices.
I would like to cache the response for specific requests.
For this reason I wrote this Filter
#Component
#Slf4j
public class CacheResponseGatewayFilterFactory extends AbstractGatewayFilterFactory<CacheResponseGatewayFilterFactory.Config> {
private final CacheManager cacheManager;
public CacheResponseGatewayFilterFactory(CacheManager cacheManager) {
super(CacheResponseGatewayFilterFactory.Config.class);
this.cacheManager = cacheManager;
}
#Override
public GatewayFilter apply(CacheResponseGatewayFilterFactory.Config config) {
final var cache = cacheManager.getCache("MyCache");
return (exchange, chain) -> {
final var path = exchange.getRequest().getPath();
if (nonNull(cache.get(path))) {
log.info("Return cached response for request: {}", path);
final var response = cache.get(path, ServerHttpResponse.class);
final var mutatedExchange = exchange.mutate().response(response).build();
return mutatedExchange.getResponse().setComplete();
}
return chain.filter(exchange).doOnSuccess(aVoid -> {
cache.put(path, exchange.getResponse());
});
};
}
When I call my rest endpoint, the first time I receive the right json, the second time I got an empty body.
What am I doing wrong?
EDIT
This is a screenshot of the exchange.getRequest() just before doing cache.put()
I solved it creating a GlobalFilter and a ServerHttpResponseDecorator. This code is caching all the responses regardless (it can be easily improved to cache only specific responses).
This is the code. However I think it can be improved. In case let me know.
#Slf4j
#Component
public class CacheFilter implements GlobalFilter, Ordered {
private final CacheManager cacheManager;
public CacheFilter(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
final var cache = cacheManager.getCache("MyCache");
final var cachedRequest = getCachedRequest(exchange.getRequest());
if (nonNull(cache.get(cachedRequest))) {
log.info("Return cached response for request: {}", cachedRequest);
final var cachedResponse = cache.get(cachedRequest, CachedResponse.class);
final var serverHttpResponse = exchange.getResponse();
serverHttpResponse.setStatusCode(cachedResponse.httpStatus);
serverHttpResponse.getHeaders().addAll(cachedResponse.headers);
final var buffer = exchange.getResponse().bufferFactory().wrap(cachedResponse.body);
return exchange.getResponse().writeWith(Flux.just(buffer));
}
final var mutatedHttpResponse = getServerHttpResponse(exchange, cache, cachedRequest);
return chain.filter(exchange.mutate().response(mutatedHttpResponse).build());
}
private ServerHttpResponse getServerHttpResponse(ServerWebExchange exchange, Cache cache, CachedRequest cachedRequest) {
final var originalResponse = exchange.getResponse();
final var dataBufferFactory = originalResponse.bufferFactory();
return new ServerHttpResponseDecorator(originalResponse) {
#NonNull
#Override
public Mono<Void> writeWith(#NonNull Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
final var flux = (Flux<? extends DataBuffer>) body;
return super.writeWith(flux.buffer().map(dataBuffers -> {
final var outputStream = new ByteArrayOutputStream();
dataBuffers.forEach(dataBuffer -> {
final var responseContent = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(responseContent);
try {
outputStream.write(responseContent);
} catch (IOException e) {
throw new RuntimeException("Error while reading response stream", e);
}
});
if (Objects.requireNonNull(getStatusCode()).is2xxSuccessful()) {
final var cachedResponse = new CachedResponse(getStatusCode(), getHeaders(), outputStream.toByteArray());
log.debug("Request {} Cached response {}", cacheKey.getPath(), new String(cachedResponse.getBody(), UTF_8));
cache.put(cacheKey, cachedResponse);
}
return dataBufferFactory.wrap(outputStream.toByteArray());
}));
}
return super.writeWith(body);
}
};
}
#Override
public int getOrder() {
return -2;
}
private CachedRequest getCachedRequest(ServerHttpRequest request) {
return CachedRequest.builder()
.method(request.getMethod())
.path(request.getPath())
.queryParams(request.getQueryParams())
.build();
}
#Value
#Builder
private static class CachedRequest {
RequestPath path;
HttpMethod method;
MultiValueMap<String, String> queryParams;
}
#Value
private static class CachedResponse {
HttpStatus httpStatus;
HttpHeaders headers;
byte[] body;
}
}

Reading response body from ServerHttpResponse Spring cloud gateway

I am trying to read response body from ServerHttpResponse in a FilterFactory class that extents AbstractGatewayFilterFactory. The method executes, but I never see the log line printed. Is this the correct approach to read response ? If yes, what am I missing here ?
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest.Builder reqBuilder = exchange.getRequest().mutate();
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
log.info("Response : {}", new String(content, StandardCharsets.UTF_8));
return bufferFactory.wrap(content);
}));
}
return super.writeWith(body);
}
};
long start = System.currentTimeMillis();
return chain.filter(exchange.mutate()
.request(reqBuilder.build())
.response(decoratedResponse)
.build());
};
}

Resources