Redirecting HTTP Requests With Zuul in Spring Boot

Zuul is part of the Spring Cloud Netflix package and allows redirect REST requests to perform various types of filters.

In almost any project where we use microservices, it is desirable that all communications between those microservices go through a communal place so that the inputs and outputs are recorded and can implement security or redirect requests, depending on various parameters.

With Zuul, this is very easy to implement since it is perfectly integrated with Spring Boot.

As always you can see the sources on which this article is based on my GitHub page. So, let’s get to it.

Creating the Project

If you have installed Eclipse with the plugin for Spring Boot (which I recommend), creating the project should be as easy as adding a new Spring Boot project type, including the Zuul Starter. To do some tests, we will also include the web starter, as seen in the image below:

We also have the option to create a Maven project from https://start.spring.io/. We will then import the necessary information from our preferred IDE.

Starting

Let’s assume that the program is listening on http://localhost: 8080/, and that we want that all the requests to the URL http://localhost: 8080/google to be redirected to https://www.google.com.

To do this we create the application.yml file in the resources directory, as seen in the image below:

This file will include the following lines:

zuul:  
  routes:
    google:
      path: /google/**
      url: https://www.google.com/

They specify that everything requested with the path /google/ and more (**) will be redirected to https://www.google.com/. If such a request is made tohttp://localhost:8080/google/search?q=profesor_pthis will be redirected tohttps://www.google.com/search?q=profesor_pIn other words, what we add after /google/ will be included in the redirection, due to the two asterisks added at the end of the path.

In order for the program to work, it will only be necessary to add the annotation @EnableZuulProxy and the start class, in this case: ZuulSpringTestApplication

import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
public class ZuulSpringTestApplication {
  public static void main(String[] args) {
  SpringApplication.run(ZuulSpringTestApplication.class, args);
  }
}

In order to demonstrate the various features of Zuul, http://localhost:8080/api will be listening to a REST service that is implemented in the TestController class  of this project. This class simply returns in the body, the data of the received request.

@RestController
public class TestController {
 final static String SALTOLINEA = "\n";
 Logger log = LoggerFactory.getLogger(TestController.class);
 @RequestMapping(path = "/api")
 public String test(HttpServletRequest request) {
  StringBuffer strLog = new StringBuffer();
  strLog.append("................ RECIBIDA PETICION EN /api ......  " + SALTOLINEA);
  strLog.append("Metodo: " + request.getMethod() + SALTOLINEA);
  strLog.append("URL: " + request.getRequestURL() + SALTOLINEA);
  strLog.append("Host Remoto: " + request.getRemoteHost() + SALTOLINEA);
  strLog.append("----- MAP ----" + SALTOLINEA);
  request.getParameterMap().forEach((key, value) -> {
   for (int n = 0; n < value.length; n++) {
    strLog.append("Clave:" + key + " Valor: " + value[n] + SALTOLINEA);
   }
  });
  strLog.append(SALTOLINEA + "----- Headers ----" + SALTOLINEA);
  Enumeration < String > nameHeaders = request.getHeaderNames();
  while (nameHeaders.hasMoreElements()) {
   String name = nameHeaders.nextElement();
   Enumeration < String > valueHeaders = request.getHeaders(name);
   while (valueHeaders.hasMoreElements()) {
    String value = valueHeaders.nextElement();
    strLog.append("Clave:" + name + " Valor: " + value + SALTOLINEA);
   }
  }
  try {
   strLog.append(SALTOLINEA + "----- BODY ----" + SALTOLINEA);
   BufferedReader reader = request.getReader();
   if (reader != null) {
    char[] linea = new char[100];
    int nCaracteres;
    while ((nCaracteres = reader.read(linea, 0, 100)) > 0) {
     strLog.append(linea);
     if (nCaracteres != 100)
      break;
    }
  }
  } catch (Throwable e) {
   e.printStackTrace();
  }
  log.info(strLog.toString());
  return SALTOLINEA + "---------- Prueba de ZUUL ------------" + SALTOLINEA +
   strLog.toString();
 }
}

Filtering: Writing Logs

In this part, we will see how to create a filter so that a record of the requests made is left.

To do this, we will create the class PreFilter.javawhich should extend ZuulFilter:

public class PreFilter extends ZuulFilter {
 Logger log = LoggerFactory.getLogger(PreFilter.class);
 @Override
 public Object run() {
  RequestContext ctx = RequestContext.getCurrentContext();
  StringBuffer strLog = new StringBuffer();
  strLog.append("\n------ NUEVA PETICION ------\n");
  strLog.append(String.format("Server: %s Metodo: %s Path: %s \n", ctx.getRequest().getServerName(), ctx.getRequest().getMethod(),
   ctx.getRequest().getRequestURI()));
  Enumeration < String > enume = ctx.getRequest().getHeaderNames();
  String header;
  while (enume.hasMoreElements()) {
   header = enume.nextElement();
   strLog.append(String.format("Headers: %s = %s \n", header, ctx.getRequest().getHeader(header)));
  };
  log.info(strLog.toString());
  return null;
 }
 @Override
 public boolean shouldFilter() {
  return true;
 }
 @Override
 public int filterOrder() {
  return FilterConstants.SEND_RESPONSE_FILTER_ORDER;
 }
 @Override
 public String filterType() {
  return "pre";
 }
}

In this class, we will overwrite the functions we see in the source. Le’s explain each of these functions:

  • public Object run() – This is run for each request received. Here we can see the contents of the request and handle it if necessary.
  • public boolean shouldFilter() – If it returns true the run function will be executed.
  • public int filterOrder() – Returns when this filter is executed because usually there are different filters for each task. We must take into account that certain redirections or changes in the petition have to be done in a certain order, by the same logic used by Zuul when processing requests.
  • public String Filtertype() – specifies when the filter is executed. If it returns “pre”, it is executed before they have made the redirect and therefore before it has been called the end server (to Google, in our example). If it returns “post”, is executed after the server has responded. In the org.springframework.cloud.netflix.zuul.filters.support.FilterConstantsclass, we have ve defined the types to be returned: PRE_TYPE, POST_TYPE, ERROR_TYPE or ROUTE_TYPE.

In the example class, we see that, before making a request to the server, some request data is recorded, leaving a log.

Finally, for Spring Boot to utilize this filter, we should add the following function in our class.

@Bean
public PreFilter preFilter() {
        return new PreFilter();
 }

Zuul looks for beans to inherit from the class, ZuulFilter, and use them.

In this example, Java’s  PostFilter class also implements another filter but only runs after making the request to the server. As I mentioned, this is achieved by returning “post” in the Filtertype() function.

For Zuul to use this class we will create another bean with a function like this:

 @Bean
 public PostFilter postFilter() {
        return new PostFilter();
 }

Remember that there is also a filter for treating errors that need to be addressed just after redirection ( “route”), but this article only looks into the post and pre filter types.

I’d like to clarify that, although this article does not with it,  Zuul can not only redirect to a static URL but also to services provided by the Eureka Server. It also integrates with Hystrix to have fault tolerance, so that if a server cannot reach you can specify what action to take.

Filtering and Implementing Security

Let us add a new file redirection to the application.yml file.

This redirection will take any request type from http: //localhost: 8080/private/foo to the page where this article (http://www.profesor-p.com) is hosted.

The line sensitiveHeaders will be explained later.

In the PreRewriteFilterclass, I have implemented another pre filter for dealing this redirection. How? Easy. Put this code in the shouldFilter() function.

@Override
public boolean shouldFilter() {
  return RequestContext.getCurrentContext().getRequest()
         .getRequestURI().startsWith("/privado");
}

Now, in the run function, we include the following code:

@Override
public Object run() {
 RequestContext ctx = RequestContext.getCurrentContext();
 StringBuffer strLog = new StringBuffer();
 strLog.append("\n------ FILTRANDO ACCESO A PRIVADO - PREREWRITE FILTER  ------\n");
 try {
  String url = UriComponentsBuilder.fromHttpUrl("http://localhost:8080/").path("/api").build().toUriString();
  String usuario = ctx.getRequest().getHeader("usuario") == null ? "" : ctx.getRequest().getHeader("usuario");
  String password = ctx.getRequest().getHeader("clave") == null ? "" : ctx.getRequest().getHeader("clave");
  if (!usuario.equals("")) {
  if (!usuario.equals("profesorp") || !password.equals("profe")) {
    String msgError = "Usuario y/o contraseña invalidos";
    strLog.append("\n" + msgError + "\n");
    ctx.setResponseBody(msgError);
    ctx.setResponseStatusCode(HttpStatus.FORBIDDEN.value());
    ctx.setSendZuulResponse(false);
    log.info(strLog.toString());
    return null;
   }
   ctx.setRouteHost(new URL(url));
  }
 } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
 }
 log.info(strLog.toString());
 return null;
}

This searches the headers of the request (headers) and, if the user header doesn’t exist, it does nothing and the request is redirected to http://www.profesor-p.com. In case there is a user header found that has the value profesorp, and the variable key has the value profe, the request is redirected to http://localhost:8080/api. Otherwise, it returns an HTTP code, forbidden, returning the string "Invalid username and/or password"in the body of the HTTP response. Moreover, the flow of the request is canceled because it calls ctx.setSendZuulResponse (false).

Because the line sensitiveHeaders in the file application.yml I mentioned above has ‘user’ and ‘password’ headers, it not be passed into the flow of the request.

It is very important that this filter is run after the PRE_DECORATION filter, because, otherwise, the call ctx.setRouteHost()will have no effect. Therefore, the  filterOrder function will have this code:

@Override
public int filterOrder() {
   return FilterConstants.PRE_DECORATION_FILTER_ORDER+1; 
}

So a call passing the user and the correct password, we will redirect to http://localhost: 8080/api.

> curl -s -H "usuario: profesorp" -H "clave: profe" localhost:8080/privado
---------- Prueba de ZUUL ------------
................ RECIBIDA PETICION EN /api ......
Metodo: GET
URL: http://localhost:8080/api
Host Remoto: 127.0.0.1
----- MAP ----
----- Headers ----
Clave:user-agent Valor: curl/7.63.0
Clave:accept Valor: */*
Clave:x-forwarded-host Valor: localhost:8080
Clave:x-forwarded-proto Valor: http
Clave:x-forwarded-prefix Valor: /privado
Clave:x-forwarded-port Valor: 8080
Clave:x-forwarded-for Valor: 0:0:0:0:0:0:0:1
Clave:accept-encoding Valor: gzip
Clave:host Valor: localhost:8080
Clave:connection Valor: Keep-Alive
----- BODY ----

If you put the wrong password the output will look like this:

 > curl -s -H "usuario: profesorp" -H "clave: ERROR" localhost:8080/privado
Usuario y/o contraseña invalidos

Filtering: Dynamic Filter

Finally, we will include two new redirections in the file applicaction.yml

local:
    path: /local/**
    url: http://localhost:8080/api
 url:
    path: /url/**
    url: http://url.com

In the first three lines,  when we go to the URL http://localhost:8080/local/XXXXwe will be redirected to http://localhost:8080/api/XXX. I’ll clarify that the label localis arbitrary and we could put json: so that this doesn’t coincide with the path that we want to redirect to.

In the second three lines, when we go to the URL http://localhost:8080/url/XXXXwe will be redirected tohttp://localhost:8080/api/XXXXX

The RouteURLFilter class will be responsible for carrying data to the URL filter. Remember that to use Zuul, the filters must create a corresponding bean.

@Bean
 public RouteURLFilter routerFilter() {
        return new RouteURLFilter();
 }

In the shouldFilter function of RouteURLFilter, we have this code to only fulfill requests to /url.

@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
if ( ctx.getRequest().getRequestURI() == null || 
        ! ctx.getRequest().getRequestURI().startsWith("/url"))
return false;
return ctx.getRouteHost() != null && ctx.sendZuulResponse();
}

In the run function, we have the code that performs the magic. Once we have captured the URL target and the path, as I explain below, it is used in the setRouteHost() function of RequestContext to properly redirect our requests.

@Override
public Object run() {
 try {
  RequestContext ctx = RequestContext.getCurrentContext();
  URIRequest uriRequest;
  try {
   uriRequest = getURIRedirection(ctx);
  } catch (ParseException k) {
   ctx.setResponseBody(k.getMessage());
   ctx.setResponseStatusCode(HttpStatus.BAD_REQUEST.value());
   ctx.setSendZuulResponse(false);
   return null;
  }
  UriComponentsBuilder uriComponent = UriComponentsBuilder.fromHttpUrl(uriRequest.getUrl());
  if (uriRequest.getPath() == null)
   uriRequest.setPath("/");
  uriComponent.path(uriRequest.getPath());
  String uri = uriComponent.build().toUriString();
  ctx.setRouteHost(new URL(uri));
 } catch (IOException k) {
  k.printStackTrace();
 }
 return null;
}

It searches the variables  hostDestino and  pathDestino in the header to make the new URL to which it must redirect.

For example, suppose we have a request like this:

> curl --header "hostDestino: http://localhost:8080" --header "pathDestino: api" \
localhost:8080/url?nombre=profesorp

The call will be redirected to http: //localhost: 8080/api?q=profesor-p and displays the following output:

--------- Prueba de ZUUL ------------
................ RECIBIDA PETICION EN /api ......
Metodo: GET
URL: http://localhost:8080/api
Host Remoto: 127.0.0.1
----- MAP ----
Clave:nombre Valor: profesorp
----- Headers ----
Clave:user-agent Valor: curl/7.60.0
Clave:accept Valor: */*
Clave:hostdestino Valor: http://localhost:8080
Clave:pathdestino Valor: api
Clave:x-forwarded-host Valor: localhost:8080
Clave:x-forwarded-proto Valor: http
Clave:x-forwarded-prefix Valor: /url
Clave:x-forwarded-port Valor: 8080
Clave:x-forwarded-for Valor: 0:0:0:0:0:0:0:1
Clave:accept-encoding Valor: gzip
Clave:host Valor: localhost:8080
Clave:connection Valor: Keep-Alive
---- BODY ----

You can also receive the URL to redirect the request body. The JSON object received must have the format defined by the  GatewayRequest class, which, in turn, contains a URIRequest object.

public class GatewayRequest {
  URIRequest uri;
  String body;
}

public class URIRequest {
  String url;
  String path;
  byte[] body=null;
}

This is an example of putting the URL redirect destination in the body:

> curl -X POST \
  'http://localhost:8080/url?nombre=profesorp' \
  -H 'Content-Type: application/json' \
  -d '{
    "body": "The body", "uri": { "url":"http://localhost:8080", "path": "api"    }
}'
  ---------- Prueba de ZUUL ------------
................ RECIBIDA PETICION EN /api ......
Metodo: POST
URL: http://localhost:8080/api
Host Remoto: 127.0.0.1
----- MAP ----
Clave:nombre Valor: profesorp
----- Headers ----
Clave:user-agent Valor: curl/7.60.0
Clave:accept Valor: */*
Clave:content-type Valor: application/json
Clave:x-forwarded-host Valor: localhost:8080
Clave:x-forwarded-proto Valor: http
Clave:x-forwarded-prefix Valor: /url
Clave:x-forwarded-port Valor: 8080
Clave:x-forwarded-for Valor: 0:0:0:0:0:0:0:1
Clave:accept-encoding Valor: gzip
Clave:content-length Valor: 91
Clave:host Valor: localhost:8080
Clave:connection Valor: Keep-Alive
----- BODY ----
The body

As the body is being dealt with, we send to the server only what is sent in the bodyparameter of the JSON request.

As shown, Zuul has a lot of power and is an excellent tool for redirections. In this article, I’ve only scratched the main features of this fantastic tool, but I hope it has allowed you to see the possibilities.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s