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.
Default (Tomcat server)
Add the next properties to
application.properties
file:server.port=8080 server.internalPort=8100 server.internalPathPrefix=/my-internal-api-path/
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; } }
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); } }
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)); } }
Run it!
Here is the simplest implementation that can be decorated for specific needs.
Jetty (Tunned approach)
Keep
1.1
,1.3
,1.4
steps from the previous approachFind 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>
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); } } } }
Here it is!
I hope it saves some time for you to have time for yourself.
Subscribe to my newsletter
Read articles from Dmytro Lazarenko directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by