Spring boot 2: multiple ports for Internal and External REST APIs + Jetty

While working on a new service, I was bumped with an interesting case. And I am sure that I am not alone in it. Most of the latest projects move to clusters; most services are usually microservice that need to be connected with other instances. Better explanation in the next picture:

Here we can see a lot of internal/private calls but also some of them are public. Internal requests can be un-secure, so it doesn't make sense to spend computation for encryption/decryption SSL connection.
These circumstances require two running HTTP servers on different ports with a custom configuration for each.

I am going to sort out two types of HTTP servers Tomcat and Jetty in Spring Boot version 2.6.6. The latest one is more lightweight at runtime. So, it is better to have a smaller memory footprint.

  1. Default (Tomcat server)

    1. Add the next properties to application.properties file:

       server.port=8080
       server.internalPort=8100
       server.internalPathPrefix=/my-internal-api-path/
      
    2. Add a component that is responsible for creating connectors from a factory:

       import org.apache.catalina.connector.Connector;
       import org.springframework.beans.factory.annotation.Value;
       import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
       import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
       import org.springframework.context.annotation.Bean;
       import org.springframework.stereotype.Component;
      
       @Component
       public class InternalHttpServer {
      
           @Value("${server.internalPort}")
           private int internalPort;
      
           @Bean
           public ServletWebServerFactory servletContainer() {
               Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
               connector.setScheme("http");
               connector.setPort(internalPort);
      
               TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
               tomcat.addAdditionalTomcatConnectors(connector);
               return tomcat;
           }
       }
      
    3. Needs to be added filter that divides endpoints into internal or external requests. In case of using an internal port for external API or vice-versa then it returns a "bad request" 404 response:

       import lombok.extern.slf4j.Slf4j;
       import org.springframework.http.HttpStatus;
      
       import javax.servlet.Filter;
       import javax.servlet.FilterChain;
       import javax.servlet.ServletException;
       import javax.servlet.ServletRequest;
       import javax.servlet.ServletResponse;
       import javax.servlet.http.HttpServletRequestWrapper;
       import javax.servlet.http.HttpServletResponse;
       import java.io.IOException;
       import java.nio.charset.StandardCharsets;
      
       @Slf4j
       public class InternalEndpointsFilter implements Filter {
      
           private final int internalPort;
           private final String internalPathPrefix;
           private final String BAD_REQUEST = String.format("{\"code\":%d,\"error\":true,\"errorMessage\":\"%s\"}",
               HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase());
      
           public InternalEndpointsFilter(int internalPort, String internalPathPrefix) {
               this.internalPort = internalPort;
               this.internalPathPrefix = internalPathPrefix;
           }
      
           @Override
           public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
               boolean isInternalAPI = ((HttpServletRequestWrapper) servletRequest).getRequestURI().startsWith(internalPathPrefix);
               boolean isInternalPort = servletRequest.getLocalPort() == internalPort;
               if ((isInternalAPI && !isInternalPort) || 
                   (!isInternalAPI && isInternalPort)) {
                   log.debug("Deny the request");
                   ((HttpServletResponse) servletResponse).setStatus(HttpStatus.BAD_REQUEST.value());
                   servletResponse.getOutputStream().write(BAD_REQUEST.getBytes(StandardCharsets.UTF_8));
                   servletResponse.getOutputStream().close();
                   return;
               }
      
               filterChain.doFilter(servletRequest, servletResponse);
           }
       }
      
    4. This filter needs to be added via the filter registration bean. Here is how to do it:

       import co.macrometa.c8kms.filter.InternalEndpointsFilter;
       import org.springframework.beans.factory.annotation.Value;
       import org.springframework.boot.web.servlet.FilterRegistrationBean;
       import org.springframework.context.annotation.Bean;
       import org.springframework.context.annotation.Configuration;
       import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
      
       @Configuration
       public class WebConfig implements WebMvcConfigurer {
      
           @Value("${server.internalPort}")
           private int internalPort;
      
           @Value("${server.internalPathPrefix}")
           private String internalPathPrefix;
      
           @Bean
           public FilterRegistrationBean<InternalEndpointsFilter> trustedEndpointsFilter() {
               return new FilterRegistrationBean<>(new InternalEndpointsFilter(internalPort, internalPathPrefix));
           }
       }
      
    5. Run it!

      Here is the simplest implementation that can be decorated for specific needs.

  2. Jetty (Tunned approach)

    1. Keep 1.1, 1.3, 1.4 steps from the previous approach

    2. Find and change the dependency in pom.xml file:

       <dependency>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
      

      to the next:

       <dependency>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot-starter-web</artifactId>
                   <exclusions>
                       <exclusion>
                           <groupId>org.springframework.boot</groupId>
                           <artifactId>spring-boot-starter-tomcat</artifactId>
                       </exclusion>
                   </exclusions>
               </dependency>
               <dependency>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot-starter-jetty</artifactId>
       </dependency>
      
    3. And add Jetty connector:

       import org.eclipse.jetty.server.Server;
       import org.eclipse.jetty.server.ServerConnector;
       import org.springframework.beans.factory.annotation.Value;
       import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer;
       import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
       import org.springframework.context.annotation.Bean;
       import org.springframework.stereotype.Component;
      
       @Component
       public class InternalHttpServer {
      
           @Value("${server.internalPort}")
           private int internalPort;
      
           @Bean
           public JettyServletWebServerFactory jettyCustomizer() {
               JettyServletWebServerFactory jetty = new JettyServletWebServerFactory();
               jetty.addServerCustomizers(new JettyCustomizer(internalPort));
               return jetty;
           }
      
           static class JettyCustomizer implements JettyServerCustomizer {
      
               private int internalPort;
      
               public JettyCustomizer(int internalPort) {
                   this.internalPort = internalPort;
               }
      
               @Override
               public void customize(Server server) {
                   ServerConnector connector = new ServerConnector(server);
                   connector.setPort(internalPort);
                   server.addConnector(connector);
                   try {
                       connector.start();
                   } catch (Exception ex) {
                       throw new IllegalStateException("Failed to start Jetty connector", ex);
                   }
               }
           }
       }
      
    4. Here it is!

I hope it saves some time for you to have time for yourself.
0
Subscribe to my newsletter

Read articles from Dmytro Lazarenko directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dmytro Lazarenko
Dmytro Lazarenko