How to add Pre Filter in Spring cloud gateway - spring-boot

I am using spring cloud gateway to route request to my downstream application
I have the router defined something like below
#Configuration
public class SpringCloudConfig {
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/user/test/**")
.uri("http://localhost:8081/test")
.id("testModule"))
.build();
}
}
Routing works fine, now I need to add a prefilter which can do some pre-condition and get routing path. but not getting how to change uri dynamically .uri("http://localhost:8081/test")
Below is the code I am trying for out in preFilter.
#Component
public class testPreFilter extends AbstractGatewayFilterFactory {
#Override
public GatewayFilter apply(Config config) {
System.out.println("inside testPreFilter.apply method");
return (exchange, chain) -> {
//get headers and do lookup for URI in mapping DB
**//If contains return modify the uri**
return chain.filter(exchange.mutate().request(request).build());
//else 401
};
}
}
so I need to forward from incoming path /user/test/** to http://localhost:8081/test1 or http://localhost:8081/test2 based on db lookup return in my custom filter

You are basically changing the path I believe , so you can do that in this fashion .
Based on the value you get from the database , set the path .

Related

FeignClient is passing on headers

I have about 10 microservices all built with Spring boot 2 using Eureka and FeignClients. My microservices use certain header values to keep track of data so when a FeignClient is used it needs to pass on certain values that are in the incoming request. So if Microservice 1 does a call to Microservice 2 it must pass on the headers from the incoming request onto microservice 2. I haven't been able to find out how I can do that. I understand their is #Header however if you have 20 FeignClients then you don't want to have to manually add the #header to all the FeignClients. Can you indicate that FeignClients must read a certain header from the incoming request and pass it on in the FeignClient?
You can use request interceptor in Feign.
Example Implementation:
Request Interceptor:
#Component
public class MyRequestInterceptor implements RequestInterceptor {
#Override
public void apply(RequestTemplate template) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String authorization = requestAttributes.getRequest().getHeader(HttpHeaders.AUTHORIZATION);
if(null != authorization) {
template.header(HttpHeaders.AUTHORIZATION, authorization);
}
}
}
Bean Configuration:
#Configuration
public class CustomFeignConfig {
#Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
#Bean
public MyRequestInterceptor basicAuthRequestInterceptor() {
return new MyRequestInterceptor();
}
#Bean
public OkHttpClient client() {
return new OkHttpClient();
}
}

Spring Cloud Gateway pass bean to custom filter

We are attempting to use Spring Cloud Gateway to setup a microservice based architecture. Currently, we have defined a route programatically:
#ServletComponentScan
#SpringBootApplication
public class GatewayApplication {
// to be passed to and used by custom filter
#Autowired
RestTemplate restTemplate;
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
#Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("status", r -> r
.method(HttpMethod.GET)
.and()
.path("/status")
.filters(f -> f.rewritePath("/status", "/v2/status")
.filter(new AuthorizationFilter(restTemplate).apply(new Config(""))))
.uri("http://localhost:8081/"))
.build();
}
}
The above would route an incoming request /status via GET to another endpoint. We would like to apply a custom filter, which we have implemented in AuthorizationFilter. This filter, as the name implies, is another microservice which will either allow or deny an incoming request based on credentials and permissions.
Currently, the pattern we are following, which works, is to inject a Spring RestTemplate into the gateway class above, and then to pass this RestTemplate to the constructor of the filter.
However, how can this be done if we wanted to switch to using a YAML file for defining all the routes? Presumably in both cases Spring would be constructing a new filter for each incoming request. But in the case of YAML, how can we pass something in the construtor? If this cannot be done, is there any other way to inject a RestTemplate, or any other resource into a custom Spring gateway filter?
You can register your own custom GatewayFilterFactory. This allows you to provide a custom configuration, and within that configuration, you can use SpEL to reference a bean.
For example:
#Component
public class AuthenticationGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthenticationGatewayFilterFactory.Config> {
public AuthenticationGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
// TODO: Implement
}
public static class Config {
private RestTemplate restTemplate;
// TODO: Getters + Setters
}
}
Now you can use SpEL to properly reference a RestTemplate bean:
spring:
cloud:
gateway:
routes:
- id: status
uri: http://localhost:8081/
filters:
- name: Authentication
args:
restTemplate: "#{#nameOfRestTemplateBean}"
predicates:
- Path=/status
Alternatively, you could inject a RestTemplate bean within your gateway filter. For example:
#Component
public class AuthenticationGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthenticationGatewayFilterFactory.Config> {
private RestTemplate restTemplate;
public AuthenticationGatewayFilterFactory(RestTemplate restTemplate) {
super(Config.class);
this.restTemplate = restTemplate;
}
#Override
public GatewayFilter apply(Config config) {
// TODO: Implement
}
public static class Config {
// TODO: Implement
}
}
The code/configuration necessary to do the inject is less complex, but it also makes it more difficult if you ever decide to put AuthenticationGatewayFilterFactory in a separate library, as the "consumers" of this library won't have any control over which RestTemplate is being injected.

Persisting Spring Cloud Gateway Routes in Database

I am currently using the spring cloud gateway project to build simple api gateway, the plan was to persist the route in mongodb, then refresh, so that the new route can be available. I have done something simple like this to get my route from mongo.
#Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder){
List<CreateAPIRequest> apiRequestList = repository.findAll();
RouteLocatorBuilder.Builder routeLocator = builder.routes();
for (CreateAPIRequest request: apiRequestList) {
routeLocator
.route(r-> {
r.path("/"+request.getProxy().getListenPath())
.filters(f->f.stripPrefix(1))
.uri(request.getProxy().getTargetUrl())
});
}
return routeLocator.build();
}
I was able to create new route in the db, but I am unable to refresh on the fly.
I need to understand how to refresh the routes on the fly.
Thanks
Whenever you wish to update the routes dynamically send a RefreshRoutesEvent. The following component implements the event sending functionality.
#Component
public class GatewayRoutesRefresher implements ApplicationEventPublisherAware {
ApplicationEventPublisher publisher;
#Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
publisher = applicationEventPublisher;
}
public void refreshRoutes() {
publisher.publishEvent(new RefreshRoutesEvent(this));
}
}
Here is a sample showing how to use the component above:
#Autowired
GatewayRoutesRefresher gatewayRoutesRefresher;
...
public void buildRoutes() {
// build your routes basing on your db entries then refresh the routes in gateway
...
gatewayRoutesRefresher.refreshRoutes();
}
You can find a more complete picture of the concept by looking into the following project code: https://github.com/botorabi/HomieCenter
SCG(Spring Cloud Gateway) has been provided RouteDefinitionRepository, you can write your own RouteDefinitionRepository, and implements RouteDefinitionRepository to override getRouteDefinitions method.
You can refer to this class: InMemoryRouteDefinitionRepository
For example:
#Service
public class MongodbDefinitionRepository implements RouteDefinitionRepository {
#Autowired
private RouteConfigDao routeConfigDao;
#Override
public Flux<RouteDefinition> getRouteDefinitions() {
// todo
List<RouteDefinition> routeConfigs = routeConfigDao.findAll();
return Flux.fromIterable(routeConfigs);
}
#Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(routeDefinition -> {
// todo
return Mono.empty();
});
}
#Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> {
// todo
int delete = routeConfigDao.delete(routeId);
if (delete > 0) {
return Mono.empty();
}
return Mono.defer(() -> Mono.error(new Exception("delete route definition error, routeId:" + routeId)));
});
}
}
How to refresh the routes on the fly
Enable actuator
place this in your application.yml
management:
endpoints:
web:
exposure:
include: gateway
POST http://ip:port/actuator/gateway/refresh
Publish RefreshRoutesEvent
#Service
public class MyPublishBiz implements ApplicationEventPublisherAware {
protected ApplicationEventPublisher publisher;
#Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public Mono<Void> refresh() {
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return Mono.empty();
}
}
I went quickly to the repo and the open issues.
And it seems that at the moment the only way to refresh the routes is from Actuator via:
/actuator/gateway/refresh
You can check the discussion here: https://github.com/spring-cloud/spring-cloud-gateway/issues/43
Can you use Consul for persisting your route definitions instead of mongo. Then a simple POST call to the actuator's refresh will reload your route definitions on the fly.

Throwing Custom Runtime exception in Spring Cloud Gateway Filters

We are using Spring Cloud Gateway with Spring Boot 2 and reactive WebFlux module.
There is an authentication filter which is added for one of the routes. Now if we throw a RuntimeException with a particular status code, it is really not picking up.
Earlier this authentication check was part of the HandlerInterceptor in Spring, but now we cannot use the web module along with WebFlux (conflict from Spring cloud gateway).
Example:
#Override
public GatewayFilter apply(Object config) {
ServerHttpRequest httpRequest = exchange.getRequest();
if(!someUtil.validRequest(httpRequest) {
throw new RuntimeException("Throw 401 Unauthorized with Custom error code and message");
}
}
Currently, the actual response always gives a 500 internal server error. From where is this coming from? Can we get hold of the errors from Filters here?
You can implement a custom error handler, and here is the Spring Boot document.
Or you can simply throw a ResponseStatusException. The default error handler will render the specific status.
Keep in mind, as of the time of writing, spring-cloud-gateway uses Spring Framework WebFlux. This means that the approach would be different. You can get hold of the exception in a filter as shown below.
Declare an exception like this:
public class UnauthorisedException extends ResponseStatusException {
public UnauthorisedException(HttpStatusCode status) {
super(status);
}
public UnauthorisedException(HttpStatusCode status, String reason) {
super(status, reason);
}
}
NB: The exception extends ResponseStatusException.
The ControllerAdvice class can be implemented as follows:
#ControllerAdvice
public class MyErrorWebExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(UnauthorisedException.class)
public Mono<ServerResponse> handleIllegalState(ServerWebExchange exchange, UnauthorisedException exc) {
exchange.getAttributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc);
return ServerResponse.from(ErrorResponse.builder(exc,HttpStatus.FORBIDDEN,exc.getMessage()).build());
}
}
In your filter you can now implement the apply method as follows:
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
if (request.getHeaders().get("token") == null){ //test is an example
throw new UnauthorisedException(HttpStatus.FORBIDDEN, "Not Authorised from Gateway");
}
ServerHttpRequest.Builder builder = request.mutate();
return chain.filter(exchange.mutate().request(builder.build()).build());
};
}

Spring cloud gateway: How to create a filter

I'm new to spring cloud gateway.
I've been watching some of the youtube videos from the SpringDeveloper channel and am working on the following example:
#Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/get")
.addRequestHeader("X-SpringOne", "Awesome")
.uri("http://httpbin.org:80"))
.build();
}
Prior to looking at spring cloud gateway, i've also looked at Spring Netflix Zuul. I understand that in Netflix Zuul, you can create filters by creating a class that extends ZuulFilter and define it as a pre, post, route, etc.
However I was wondering how one can create a PRE/ POST filter using Spring cloud gateway?
Any help/ advice is much appreciated.
Thanks.
For a pre filter here is AddRequestHeader (code is executed before chain.filter() call):
public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
#Override
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate()
.header(config.getName(), config.getValue())
.build();
return chain.filter(exchange.mutate().request(request).build());
};
}
}
For a 'post' filter, here is SetStatus (code is run in lambda in chain.filter(exchange).then()):
public class SetStatusGatewayFilterFactory extends AbstractGatewayFilterFactory<SetStatusGatewayFilterFactory.Config> {
#Override
public GatewayFilter apply(Config config) {
final HttpStatus status = ServerWebExchangeUtils.parse(config.status);
return (exchange, chain) -> {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// check not really needed, since it is guarded in setStatusCode,
// but it's a good example
if (!exchange.getResponse().isCommitted()) {
setResponseStatus(exchange, status);
}
}));
};
}
}
Here is a simple example in Kotlin: the URI http://.../customers is mapped to the URI obtained from the discovery service (lb = load balanced) for the service named customer and appended with "/". Furthermore, the forwarded request is enhanced with an additional header entry. Hope this helps.
#SpringBootApplication
class Application {
#Bean
fun routes(builder: RouteLocatorBuilder) = builder.routes {
route {
path("/customers")
filters {
setPath("/")
addRequestHeader("aKey", "aValue")
}
uri("lb://customer")
}
}
}
I am not sure this is the correct way to do it because I am also trying to achieve this behavior, I am thinking if this is something that needs to be done:
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class CustomFilter implements GatewayFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//code for PRE filter
Mono<Void> v = chain.filter(exchange);
//code for POST filter
return v;
}
}
Let me know if that works for you or if you found another solution.

Resources