Redirecting HTTP Requests With Zuul in Spring Boot
In this post, we take a look at how to use Zuul and Spring Boot in microservices applications in order to redirect HTTP requests.
Join the DZone community and get the full member experience.
Join For FreeZuul 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_p
this will be redirected tohttps://www.google.com/search?q=profesor_p
. In 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.java
which 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.FilterConstants
class, 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 PreRewriteFilter
class, 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/XXXX
we will be redirected to http://localhost:8080/api/XXX
. I'll clarify that the label local
is 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/XXXX
we 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 body
parameter 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.
This article was written originally in Spanish and you can find it here.
You can find me on Twitter here.
Opinions expressed by DZone contributors are their own.
Comments