Getting Started with Zuul
It’s been a while since my last post! I recently have been reading a lot about the idea of “API management” or an “API gateway”. There’s a lot of commercial offerings in this field. Many of them promise you (to some extend) ultimate flexibility and endless possibilities. My preference is for “lean and mean” approaches where I can pick the building blocks that I need. In the long run, that offers more flexibility. After all, you could replace building blocks. Having small building blocks makes it less tempting to put any kind of business logic in such a gateway. Doing that must sooner or later lead to some kind of vendor lock-in.
Then Zuul caught my attention. It is an open-source project developed and used by Netflix. They use it for building edge services. An edge service is like a front door for all requests from devices and web sites to your back end systems. So it seemed like a great fit to build a light-weight gateway myself, and I decided to give it a try.
The big picture
To get going, I thought about a very simple back end service. According to ancient traditions of our people, we should build something that only says “Hello world”. In this simple scenario, we’ll put an API gateway in front of that. The gateway will determine who to greet.
Building the back end
To build the back end, I choose Spark. Spark is a small framework for creating web applications. Small applies to the framework itself as well as the amount of code needed to get started.
The packaged application is a little above 3 megabyte. Compare that to 14 megabyte that Spring Boot gives you ;-). To be honest, Spring gives you a lot more features, but for this example I don’t need the extra stuff.
As I said, the code is also small. The application is only one class:
@Slf4j
public class App {
public static void main(String[] args) {
// 1. Register a filter that reads a custom HTTP header added by the gateway.
before("/*", (req, res) -> {
final String username = req.headers("username");
req.attribute("username", username);
log.info("Processing request for user {}", username);
});
// 2. Handle requests by replying with a friendly greeting.
get("/*", "text/html", (req, res) -> {
final String username = req.attribute("username");
final String greeting = Optional.ofNullable(username).orElse("world");
return String.format("Hello, %s", greeting);
});
}
}
Building the gateway
As a starting point for working with Zuul, I’ve looked at Spring Cloud. Spring Cloud has a lot of sub-projects, for example Spring Cloud Netflix. This sub-project provides integrations with various Netflix OSS components, like Zuul.
Getting started is pretty easy. It requires one class and one configuration file:
package tk.mulders.blog.zuul.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@EnableZuulProxy
@SpringBootApplication
public class Gateway {
public static void main(String[] args) {
SpringApplication.run(Gateway.class, args);
}
}
And the configuration file:
management.security.enabled=false
ribbon.eureka.enabled=false
server.port=8080
zuul.routes.hello.url=http://localhost:4567
The interesting stuff here is the last line in the configuration file.
It tells Zuul to route any requests to /hello
to http://localhost:4567
.
If we start this gateway, we have a working application ready. We can use curl or Postman to test it. I prefer curl, so let’s use that for now.
# invoke the back end directly
curl http://localhost:4567/hello
Hello, world
# invoke the back end through the gateway
curl http://localhost:8080/hello
Hello, world
Dummy authentication in the gateway
This looks cool already, but the gateway doesn’t do that much. Let’s try to make it authenticate users. Adding production-ready authentication is a complex process. Instead, I’ll add dummy authentication, to illustrate how it works.
The dummy authentication will be very simple.
Any request with a cookie named sessionId
is an “authenticated” request.
To process this cookie, we add a new bean to our Spring gateway:
package tk.mulders.blog.zuul.gateway.filters;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
@Slf4j
public class AuthenticatedUserFilter extends ZuulFilter {
// This filter runs before routing the request to the back end.
@Override
public String filterType() { return "pre"; }
@Override
// An arbitrary value. It only matters when comparing it with other filters.
public int filterOrder() { return 5; }
@Override
// You could write code to determine whether this filter must process the request.
public boolean shouldFilter() { return true; }
@Override
public Object run() {
final HttpServletRequest request = RequestContext.getCurrentContext()
.getRequest();
final Cookie[] cookies = request.getCookies();
if (cookies != null) {
findSessionCookie(cookies)
.map(this::findSession)
.ifPresent(this::addAuthenticationHeaders);
}
return null;
}
private Map<String, String> findSession(final Cookie cookie) {
return Collections.singletonMap("username", "dummy");
}
private void addAuthenticationHeaders(final Map<String, String> authentication) {
final RequestContext requestContext = RequestContext.getCurrentContext();
authentication.forEach(requestContext::addZuulRequestHeader);
}
private Optional<Cookie> findSessionCookie(final Cookie[] cookies) {
return Arrays.stream(cookies)
.filter(cookie -> "sessionId".equalsIgnoreCase(cookie.getName()))
.findFirst();
}
}
The interesting parts are the run()
method, and the methods that it invokes.
The run()
method tries to find the relevant cookie using the findSessionCookie()
method.
As I described, this method gives the cookie whose name is sessionId
.
If there is no cookie with that name, it returns an empty value.
The findSession()
method is a placeholder.
It receives the cookie with the sessionId
.
You could use this method to lookup an active user session, for example in a database.
But that is subject for another post.
We represent an authenticated session by a Map<String, String>
.
For now, this method returns one pair: username -> dummy
.
Finally, run()
invokes the addAuthenticationHeaders()
method.
Its goal is to convert the authenticated session (a Map
) into headers for the HTTP-request to the back end.
For now, it adds a header for each entry in the Map
.
Do not forget to register this filter as a Spring bean.
If you don’t do this, it will never be invoked.
Add the following to Gateway.java
:
@Bean
public AuthenticatedUserFilter authenticatedUserFilter() {
return new AuthenticatedUserFilter();
}
Putting the gateway to the test
If you use curl, you can add cookies to any request using the --cookie
switch.
We would expect the result to change from “Hello, world” to “Hello, dummy”.
curl http://localhost:8080/hello --cookie "sessionId=example"
Hello, dummy
And indeed, the gateway works as we expected!
Conclusion
In this article, we have written a proof-of-concept for a simple API gateway. It shows how to transfer authentication details from the gateway to a back end system. It also provides extension points for looking up user sessions. They could, for example, live in an external source, such as a database.
The full code is available on GitHub.