How to get Request header values in Spring Cloud Gateway - spring-boot

I am implementing API routing using spring cloud gateway, in one of the use cases I need to get the header value from incoming request and use it for some processing, further add this processed value to outgoing (routed) API call as header. How to get the header value from an incoming API call in routeBuilder?
#Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeBuilder) {
return routeBuilder.routes()
.route(r -> r.path("/api/v1/**")
.setRequestHeader("testKey", "testValue")
.uri("URL"))
.build();
}

You Can write a custom filter for the same. Its just a way around, not sure what is the best way for doing this:
public class SomeFilterFactory
extends AbstractGatewayFilterFactory<SomeFilterFactory.SomeConfig> {
public SomeFilterFactory() {
super(SomeFilterFactory.SomeConfig.class);
}
#Override
public GatewayFilter apply(SomeFilterFactory.SomeConfig config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String someHeader = request.getHeaders().getFirst("someHeader");
// do your things here
return chain.filter(exchange);
};
}
public static class SomeConfig {
// your config if required
// or use name value config
}
}

Get incoming request/response from Predicate.
#Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route("default-api-route", new Function<PredicateSpec, Route.AsyncBuilder>() {
#Override
public Route.AsyncBuilder apply(PredicateSpec predicateSpec) {
return predicateSpec.predicate(new Predicate<ServerWebExchange>() {
#Override
public boolean test(ServerWebExchange serverWebExchange) {
// get request header here
return false;
}
}).uri("http://httpbin.org").order(10000);
}
}).build();
}

Related

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 filter read and modify response body

I am trying to read and modify the response body in the filter. I have added a custom filter to read and modify the changes but didn't found any option to read the body part. I can change the headers and cookie but not the body of the response
#Configuration
public class GatewayConfiguration {
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder, CustomGatewayFilterFactory extractFilter) {
return builder.routes()
.route("ccprest",r -> r.path("/api/details/**").
filters( f -> f.filter(extractFilter.apply(new ExtractCCPUrlGatewayFilterFactory.Config()))).uri(http://localhost:8090/test))
.build();
}
Custom filter
#CommonsLog
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory< CustomGatewayFilterFactory.Config> {
protected static final ObjectMapper MAPPER = new ObjectMapper();
public CustomGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter((exchange, chain) -> {
return chain.filter(exchange.mutate().request(request).build()).s
then(
Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
//response.getBody() //Dont know how to read and modify the body
Optional.ofNullable(exchange.getRequest()
.getQueryParams()
.getFirst("include-total"))
.ifPresent(qp -> {
String responseContentLanguage = "hai";
response.getHeaders()
.add("Bael-Custom-Language-Header", responseContentLanguage);
});
}));
}, 10);
}
public static class Config {
}
}

How to rewrite URLs with Spring (Boot) via REST Controllers?

Let's say I have the following controller with its parent class:
#RestController
public class BusinessController extends RootController {
#GetMapping(value = "users", produces = {"application/json"})
#ResponseBody
public String users() {
return "{ \"users\": [] }"
}
#GetMapping(value = "companies", produces = {"application/json"})
#ResponseBody
public String companies() {
return "{ \"companies\": [] }"
}
}
#RestController
#RequestMapping(path = "api")
public class RootController {
}
Data is retrieved by calling such URL's:
http://app.company.com/api/users
http://app.company.com/api/companies
Now let's say I want to rename the /api path to /rest but keep it "available" by returning a 301 HTTP status code alongside the new URI's
e.g. client request:
GET /api/users HTTP/1.1
Host: app.company.com
server request:
HTTP/1.1 301 Moved Permanently
Location: http://app.company.com/rest/users
So I plan to change from "api" to "rest" in my parent controller:
#RestController
#RequestMapping(path = "rest")
public class RootController {
}
then introduce a "legacy" controller:
#RestController
#RequestMapping(path = "api")
public class LegacyRootController {
}
but now how to make it "rewrite" the "legacy" URI's?
That's what I'm struggling with, I can't find anything Spring-related on the matter, whether on StackOverflow or elsewhere.
Also I have many controllers AND many methods-endpoints so I can not do this manually (i.e. by editing every #RequestMapping/#GetMapping annotations).
And project I'm working on is based on Spring Boot 2.1
Edit: I removed the /business path because actually inheritance doesn't work "by default" (see questions & answers like Spring MVC #RequestMapping Inheritance or Modifying #RequestMappings on startup ) - sorry for that.
I finally found a way to implement this, both as a javax.servlet.Filter AND a org.springframework.web.server.WebFilter implementation.
In fact, I introduced the Adapter pattern in order to transform both:
org.springframework.http.server.ServletServerHttpResponse (non-reactive) and
org.springframework.http.server.reactive.ServerHttpResponse (reactive)
because on the contrary of the Spring's HTTP requests' wrappers which share org.springframework.http.HttpRequest (letting me access both URI and HttpHeaders), the responses's wrappers do not share a common interface that does it, so I had to emulate one (here purposely named in a similar fashion, HttpResponse).
#Component
public class RestRedirectWebFilter implements Filter, WebFilter {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
ServletServerHttpRequest request = new ServletServerHttpRequest((HttpServletRequest) servletRequest);
ServletServerHttpResponse response = new ServletServerHttpResponse((HttpServletResponse) servletResponse);
if (actualFilter(request, adapt(response))) {
chain.doFilter(servletRequest, servletResponse);
}
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (actualFilter(exchange.getRequest(), adapt(exchange.getResponse()))) {
return chain.filter(exchange);
} else {
return Mono.empty();
}
}
/**
* Actual filtering.
*
* #param request
* #param response
* #return boolean flag specifying if filter chaining should continue.
*/
private boolean actualFilter(HttpRequest request, HttpResponse response) {
URI uri = request.getURI();
String path = uri.getPath();
if (path.startsWith("/api/")) {
String newPath = path.replaceFirst("/api/", "/rest/");
URI location = UriComponentsBuilder.fromUri(uri).replacePath(newPath).build().toUri();
response.getHeaders().setLocation(location);
response.setStatusCode(HttpStatus.MOVED_PERMANENTLY);
response.flush();
return false;
}
return true;
}
interface HttpResponse extends HttpMessage {
void setStatusCode(HttpStatus status);
void flush();
}
private HttpResponse adapt(ServletServerHttpResponse response) {
return new HttpResponse() {
public HttpHeaders getHeaders() {
return response.getHeaders();
}
public void setStatusCode(HttpStatus status) {
response.setStatusCode(status);
}
public void flush() {
response.close();
}
};
}
private HttpResponse adapt(org.springframework.http.server.reactive.ServerHttpResponse response) {
return new HttpResponse() {
public HttpHeaders getHeaders() {
return response.getHeaders();
}
public void setStatusCode(HttpStatus status) {
response.setStatusCode(status);
}
public void flush() {
response.setComplete();
}
};
}
}
Since it looks like you want to preserve the 301 but also have it return a response, you do have the option to wire in your RootController into your LegacyRootController
That way you can provide reuse the logic you have in the RootController but return different response codes and serve different paths on your LegacyRootController
#RestController
#RequestMapping(path = "api")
public class LegacyRootController {
private final RootController rootController;
public LegacyRootController(RootController rootController) {
this.rootController = rootController;
}
#GetMapping(value = "users", produces = {"application/json"})
#ResponseStatus(HttpStatus.MOVED_PERMANENTLY) // Respond 301
#ResponseBody
public String users() {
return rootController.users(); // Use rootController to provide appropriate response.
}
#GetMapping(value = "companies", produces = {"application/json"})
#ResponseStatus(HttpStatus.MOVED_PERMANENTLY)
#ResponseBody
public String companies() {
return rootController.companies();
}
}
This will allow you to serve /api/users to serve up a response with a 301, while also allowing you to serve /rest/users with your standard response.
If you would like to add the Location headers, you can have your LegacyRootController return a ResponseEntity to provide the body, code and header values.
#GetMapping(value = "users", produces = {"application/json"})
public ResponseEntity<String> users() {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setLocation("...");
return new ResponseEntity<String>(rootController.users(), responseHeaders, HttpStatus.MOVED_PERMANENTLY);
}
If you want to serve multiple endpoints that does not serve different status codes, you can simply provide multiple paths
#RequestMapping(path = {"api", "rest"})

Spring Integration for TCP Socket Client using Dynamic Host and Port

I have a working example of TCPSocketClient using Spring Integration with hard coded host name and the port.
How to modify this example to accept the localhost and the port 5877 to be passed dynamically?
i.e. Is it possible to call the exchange method like ExchangeService.exchange(hostname, port, request) instead of ExchangeService.exchange(request)
If so how does those parameter be applied on to the client bean?
#Configuration
public class AppConfig {
#Bean
public IntegrationFlow client() {
return IntegrationFlows.from(ApiGateway.class).handle(
Tcp.outboundGateway(
Tcp.netClient("localhost", 5877)
.serializer(codec())
.deserializer(codec())
).remoteTimeout(10000)
)
.transform(Transformers.objectToString())
.get();
}
#Bean
public ByteArrayCrLfSerializer codec() {
ByteArrayCrLfSerializer crLfSerializer = new ByteArrayCrLfSerializer();
crLfSerializer.setMaxMessageSize(204800000);
return crLfSerializer;
}
#Bean
#DependsOn("client")
public ExchangeService exchangeService(ApiGateway apiGateway) {
return new ExchangeServiceImpl(apiGateway);
}
}
public interface ApiGateway {
String exchange(String out);
}
public interface ExchangeService {
public String exchange(String request);
}
#Service
public class ExchangeServiceImpl implements ExchangeService {
private ApiGateway apiGateway;
#Autowired
public ExchangeServiceImpl(ApiGateway apiGateway) {
this.apiGateway=apiGateway;
}
#Override
public String exchange(String request) {
String response = null;
try {
response = apiGateway.exchange(request);
} catch (Exception e) {
throw e;
}
return response;
}
}
For dynamic processing you may consider to use a Dynamic Flows feature from Spring Integration Java DSL: https://docs.spring.io/spring-integration/docs/current/reference/html/#java-dsl-runtime-flows
So, whenever you receive a request with those parameters, you create an IntegrationFlow on the fly and register it with the IntegrationFlowContext. Frankly, we have exactly sample in the docs for your TCP use-case:
IntegrationFlow flow = f -> f
.handle(Tcp.outboundGateway(Tcp.netClient("localhost", this.server1.getPort())
.serializer(TcpCodecs.crlf())
.deserializer(TcpCodecs.lengthHeader1())
.id("client1"))
.remoteTimeout(m -> 5000))
.transform(Transformers.objectToString());
IntegrationFlowRegistration theFlow = this.flowContext.registration(flow).register();

Zuul redirect to external link

Using zuul is possible to redirect a request to an external link like http://www.google.com ?
I have this scenario.
In a webpage there are a bunch of links pointing to a several websites. When you click to one of these zuul checks if you have the permission to visit this page and redirect the browser to the external link.
I've created a route filter.
public class TestZuulFilter extends ZuulFilter {
#Override
public String filterType() {
return "route";
}
#Override
public int filterOrder() {
return 5;
}
#Override
public boolean shouldFilter() {
// ... filter logic ...
}
#Override
public Object run() {
// ... permission check ...
RequestContext ctx = RequestContext.getCurrentContext();
//todo redirect
}
}
How can i redirect the user browser to google.com ?
Thank you.
Update 20/09/2016
I've managed to solve my problem changing filter type from "pre" to "post" and adding the Location HTTP header to the response.
public class TestZuulFilter extends ZuulFilter {
#Override
public String filterType() {
return "post";
}
#Override
public int filterOrder() {
return 5;
}
#Override
public boolean shouldFilter() {
// ... filter logic ...
}
#Override
public Object run() {
// ... permission check ...
RequestContext ctx = RequestContext.getCurrentContext();
//redirect
HttpServletResponse response = ctx.getResponse();
response.setStatus(HttpServletResponse.SC_FOUND);
response.setHeader("Location", "http://www.google.com");
return null;
}
}
Now it works, but is this the right way to do it ?

Resources