How to make Zuul dynamic routing based on HTTP method and resolve target host by 'serviceId'? - spring

How to make Zuul dynamic routing based on HTTP method (GET/POST/PUT...)?
For example, when you need to route the POST request to the different host instead of the default one described in 'zuul.routes.*'...
zuul:
routes:
first-service:
path: /first/**
serviceId: first-service
stripPrefix: false
second-service:
path: /second/**
serviceId: second-service
stripPrefix: false
I.e. when we request 'GET /first' then Zuul route the request to the 'first-service', but if we request 'POST /first' then Zuul route the request to the 'second-service'.

To implement dynamic routing based on HTTP method we can create a custom 'route' type ZuulFilter and resolve 'serviceId' through DiscoveryClient. Fore example:
#Component
public class PostFilter extends ZuulFilter {
private static final String REQUEST_PATH = "/first";
private static final String TARGET_SERVICE = "second-service";
private static final String HTTP_METHOD = "POST";
private final DiscoveryClient discoveryClient;
public PostOrdersFilter(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
#Override
public String filterType() {
return "route";
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String method = request.getMethod();
String requestURI = request.getRequestURI();
return HTTP_METHOD.equalsIgnoreCase(method) && requestURI.startsWith(REQUEST_PATH);
}
#Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
List<ServiceInstance> instances = discoveryClient.getInstances(TARGET_SERVICE);
try {
if (instances != null && instances.size() > 0) {
context.setRouteHost(instances.get(0).getUri().toURL());
} else {
throw new IllegalStateException("Target service instance not found!");
}
} catch (Exception e) {
throw new IllegalArgumentException("Couldn't get service URL!", e);
}
return null;
}
}

#Cepr0's solution is right. Here I am proposing just a simpler way (without service discovery). Assuming you have that route:
zuul:
routes:
first:
path: /first/**
# No need for service id or url
Then you can route requests for '/first' route in 'route' type filter just by setting location to request context.
#Component
public class RoutingFilter extends ZuulFilter {
#Override
public String filterType() {
return ROUTE_TYPE;
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() throws ZuulException {
/* Routing logic goes here */
URL location = getRightLocationForRequest();
ctx.setRouteHost(location);
return null;
}
}

Related

Zuul path - SpringBoot detects it although not configured

I followed the sample from the Spring Cloud docu and have now configured in my application.yml
server:
port: 18081
servlet:
context-path: /myPath/services
zuul:
# don't add per default all services automatically to the Zuul server
ignoredServices: '*'
routes:
mytest:
#path: /checkPath/**
url: http://mytest.server.local/api/
and a configured Filter:
public class PreFilter extends ZuulFilter {
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 1;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
System.out.println("Request Method : " + request.getMethod() + " Request URL : " + request.getRequestURL().toString());
return null;
}
}
When I test with
http://localhost:18081/myPath/services/mytest/test
I would expect that this works only if path is configured. If I leave it as above (uncommented) I get from ZuulPreFilter that it was hit. I wouldn't expect it since the path shouldn't be found. Is there a misunderstanding or do I miss something in my config? Why is the path hit at all because .../checkPath2/... won't be detected?

Spring Cloud Canary Deployment

I have a spring cloud micro service with Zuul running on docker.
Requirement:
I want to create canary deployment with specific requirement as we will have x clients and I want to canary test with y specific clients (using email or username).
Can I configure the gateway to route requests to the new version of the micro-service for these y clients?
So you can do that via configuration or dynamic routing but i think first idom is not good for generic part every client you have to define it again and again but second one is more good
#Component
public class PostFilter extends ZuulFilter {
private static final String REQUEST_PATH = "/special-customer-product-request-url";
private static final String TARGET_SERVICE = "special-customer-service";
private static final String HTTP_METHOD = "POST or GET";
private final DiscoveryClient discoveryClient;
public PostOrdersFilter(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
#Override
public String filterType() {
return "route";
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String method = request.getMethod();
String requestURI = request.getRequestURI();
return HTTP_METHOD.equalsIgnoreCase(method) && requestURI.startsWith(REQUEST_PATH);
}
#Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
List<ServiceInstance> instances = discoveryClient.getInstances(TARGET_SERVICE);
try {
if (instances != null && instances.size() > 0) {
context.setRouteHost(instances.get(0).getUri().toURL());
} else {
throw new IllegalStateException("Target service instance not found!");
}
} catch (Exception e) {
throw new IllegalArgumentException("Couldn't get service URL!", e);
}
return null;
}
}

How i can pass new Object between Zuul filters, using spring context?

Currently, I have AuthFilter and here I received an UserState. I need to pass it to the next Filter. But how to do it right? Or exists other practices to resolve it?
public class AuthFilter extends ZuulFilter {
#Autowired
private AuthService authService;
#Autowired
private ApplicationContext appContext;
#Override
public String filterType() {
return PRE_TYPE;
}
#Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 2;
}
#Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
String requestURI = context.getRequest().getRequestURI();
for (String authPath : authPaths) {
if (requestURI.contains(authPath)) return true;
}
return false;
}
#Override
public Object run() throws ZuulException {
try {
UserState userState = authService.getUserData();
DefaultListableBeanFactory context = new DefaultListableBeanFactory();
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(UserState.class);
beanDefinition.setPropertyValues(new MutablePropertyValues() {
{
add("user", userState);
}
});
context.registerBeanDefinition("userState", beanDefinition);
} catch (UndeclaredThrowableException e) {
if (e.getUndeclaredThrowable().getClass() == UnauthorizedException.class) {
throw new UnauthorizedException(e.getMessage());
}
if (e.getUndeclaredThrowable().getClass() == ForbiddenException.class) {
throw new ForbiddenException(e.getMessage(), "The user is not allowed to make this request");
}
}
return null;
}
}
I pretty sure filters are chained together and the request/response are passed through them. You can add the data to the request, and have the next filter look for it.

zuul return 302 with location header

I have a simple use case, where I need to send 302 HTTP status with the Location header if the request comes to the gateway when the url contains /logout. This has to happen without routing to the back-end service.
Below is my zuul fillter:
public class LogoutFillter extends ZuulFilter{
#Override
public boolean shouldFilter() {
if(RequestContext.getCurrentContext().getRequest().getRequestURI().toLowerCase().contains("/logout")){
return true;
}else{
return false;
}
}
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 3;
}
#Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpSession excistingSession = context.getRequest().getSession(false);
if(excistingSession != null){
excistingSession.invalidate();
context.unset()
//context.addZuulResponseHeader("Location", "/abc/def/logout.do"); //notworking
context.setResponseStatusCode(302);
}
return null;
}
}
I have tried to do this like below:
HttpServletResponse response = context.getResponse();
response.setStatus(302)
response.setHeader("Location", "/abc/logout.to");
context.unset();
context.setResponse(response);
However, that didn't work either. Any suggestion would be appreciated.
Finally I managed to resolve the issue. Below is my code
#Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpSession excistingSession = context.getRequest().getSession(false);
if(excistingSession != null){
excistingSession.invalidate();
context.setSendZuulResponse(false);
context.addZuulResponseHeader("Location", "/abc/def/logout.do");
context.setResponseStatusCode(HttpServletResponse.SC_MOVED_TEMPORARILY);
}
return null;
}
In the above code setSendZuulResponse(false) will stop the routing to the back-end service, and the addZuulResponseHeader will add the response header.

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