Dynamic Routing Through Zuul With a REST API and Spring Boot Without Spring Config — Sub-Domain to Subpath Router

Problem Statement

Say there was a requirement to forward all the traffic coming to *.adomain.com to adomain.com/*. For Example: a.adomain.com/**/* should be forwarded to adomain.com/a/**/*. Additionally, the proxy gateway should be up all the time and new subdomains need to be registered in the gateway without any downtime. This problem was a very specific one and needed some research to achieve the same. 

Research Around Proxies

We tried a couple of things, including node http-proxy, the Java throo library (internally using Zuul), and Zuul itself. All the examples were based on prefetched configuration from a properties file for the proxy creation, except advance Zuul examples, which talk about refreshable configuration using Spring refreshable cloud configuration and RabbitMQ. There was a need for a runtime proxy creator service, preferably exposing a POST API taking some parameters and registering a new proxy at runtime. Even a delete API was required to get rid of proxies no longer being used. Being a Java developer, the natural choice was Zuul.

Pre-Requisites

Knowledge of Java, APIs, proxy, Apache, the gateway concept, and Spring Boot is needed to fully grasp the idea presented here.

Solution

While going through Zuul provided by Netflix, now added in Spring Cloud distribution, there is a nice example of how to do it by using Eureka, Zuul Server, Spring Cloud Server, RabbitMQ, and Git. Although the whole example is great and provides great flexibility, it involves many frameworks and lots of setup to be done. I could not use it as I was looking to create a very simple solution to solve our problem, considering the tradeoff of not being HA.

The architecture is as follows:

Image title

Coding

There are a couple of things needed to make the application work:

APIs

To take the parameter in POST for creating the route.

Zuul-Related Changes

<project
    xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>gateway-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.7.RELEASE</version>
        <relativePath />
        <!-- lookup parent from repository -->
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <!-- Dependencies -->
        <spring-cloud.version>Camden.SR7</spring-cloud.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-shade-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>META-INF/spring.handlers</resource>
                                </transformer>
                                <transformer
implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
                                    <resource>META-INF/spring.factories</resource>
                                </transformer>
                                <transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>META-INF/spring.schemas</resource>
                                </transformer>
                                <transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
                                <transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.example.application.SpringBootWebApplication</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Spring Boot Application class:

package com.mettl.gatewayservice.application;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@EnableAutoConfiguration(exclude = { RabbitAutoConfiguration.class })
@EnableZuulProxy
@ComponentScan("com.example.gatewayservice")
public class SpringBootWebApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootWebApplication.class, args);
    }
}

application.yml

server:
  port: ${appPort:80}
info.app.version: @project.version@
# Actuator endpoint path (/admin/info, /admin/health, ...)
server.servlet-path: /
management.context-path: /admin
# ribbon.eureka.enabled: false
zuul:
  ignoredPatterns: /**/admin/**, /proxyurl
  routes:
    zuulDemo1:
      path: /**
      url: http://localhost:8000/
# stripPrefix set to true if context path is set to /
      stripPrefix: true

There are two parts:

1. Registration of Proxy Routes

This part is done by creating a service class by auto-wiring two dependencies, like this:  

@Service    
public class ZuulDynamicRoutingService {         

private static final String HTTP = "http://";         
private final ZuulProperties zuulProperties;         
private final ZuulHandlerMapping zuulHandlerMapping;

  ......

Adding the new route is achieved by the following code.

Creation of uuid can be done by following any standard way. The only thing is that it should be a unique key, as it is going to be used in the map used by zuulProperties.getRoutes().

String uuid = GenerateUID.getUID();
if (StringUtils.isEmpty(dynamicRouteRequest.getSubpath())) {
    dynamicRouteRequest.setSubpath("");
}

String url = "http://" + dynamicRouteRequest.getHost() + ":" + dynamicRouteRequest.getPort() + dynamicRouteRequest.getSubpath();
zuulProperties.getRoutes().put(uuid, new ZuulRoute(uuid, "/" + uuid + "/**", null, url, true, false, new HashSet<>()));
zuulHandlerMapping.setDirty(true);

This service is injected in a controller to receive the post request and forward the details to the service class to get the proxy created.

It can be checked with the URL /admin/routes.

2. Forwarding HTTP Requests to the Destination With Subdomain to Subpath Conversion

This requires a PreFilter class extending ZuulFilter:

@Component
public class PreFilter extends ZuulFilter {

  private static Logger log = LoggerFactory.getLogger(PreFilter.class);

  private UrlPathHelper urlPathHelper = new UrlPathHelper();

  @Override
  public String filterType() {
      return "pre";
  }

  @Override
  public int filterOrder() {
      return 1;
  }

  @Override
  public boolean shouldFilter() {

      RequestContext ctx = RequestContext.getCurrentContext();
      String requestURL = ctx.getRequest().getRequestURL().toString();
      //Here we only require to filter those URLs which contains "proxyurl" and "/admin/".
      return !(requestURL.contains("proxyurl") || requestURL.contains("/admin/"));
  }

  //The actual part where the subdomain to subpath conversion happens is as follows:

  @Override
  public Object run() {
      RequestContext ctx = RequestContext.getCurrentContext();
      String remoteHost = ctx.getRequest().getRemoteHost();
      String requestURL = ctx.getRequest().getRequestURL().toString();
      if (!requestURL.contains("proxyurl")) {
          log.info("remoteHost {} requestURL {}", new Object[]{remoteHost, requestURL});
          String originatingRequestUri = this.urlPathHelper.getOriginatingRequestUri(ctx.getRequest());
          final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
          log.info("URI {} original URI {}", new Object[]{requestURI, originatingRequestUri});
          String protocol = requestURL.substring(0, requestURL.indexOf("//") + 2);
          String urlWithoutProtocol = requestURL.substring(requestURL.indexOf("//") + 2);
          String[] split = urlWithoutProtocol.substring(0, urlWithoutProtocol.indexOf("/")).split("\\.");
          String subPath = split[0];
          final String newURL = protocol + "." + split[1] + "." + split[2];
          //Here the main thing is to create a HttpServletRequestWrapper and override the request coming from the actual request
          HttpServletRequestWrapper httpServletRequestWrapper = new HttpServletRequestWrapper(ctx.getRequest()) {
              public String getRequestURI() {
                  if (requestURI != null && !requestURI.equals("/")) {
                      if (!StringUtils.isEmpty(subPath)) {
                          return "/" + subPath + requestURI;
                      } else {
                          return requestURI;
                      }
                  }
                  if (!StringUtils.isEmpty(subPath)) {
                      return "/" + subPath;
                  } else {
                      return "/";
                  }
              }
              public StringBuffer getRequestURL() {
                return new StringBuffer(newURL);
              }
          };
          ctx.setRequest(httpServletRequestWrapper);
          HttpServletRequest request = ctx.getRequest();
          log.info("PreFilter: " + String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
       }
       return null;
    }
}

Now you can run the application by adding the missing part with mvn springboot:run or java -jar gateway-service.jar.

Now, use Postman to submit the request, like below.

Image title

To Be Done on Your Own

Summary

We learned to

  1. Create a proxy route at runtime by exposing an API 

  2. Change PreFilter (a subclass of ZuulFilter) to pre-process the HTTP request

  3. Add Apache configuration to forward requests to Zuul Gateway

 

 

 

 

Top