1

I have a Spring Boot rest service based application configured on multiple ports that needs to distinguish each request between the port it came through. The idea of having several ports for the application is due to different public and private sub-networks (with different security access levels) that could access different parts of the services exposed by the application.

Conceptually the idea was to add additional connectors to the embedded tomcat and then catch all incoming requests altering them by adding a custom header to each one specifying the "channel" it came through.

The problem I'm facing is that I have no idea how I could catch these incoming requests on a connector level (before it gets to any filter or servlet).

So, for the multi-port solution I have:

@Configuration
public class EmbeddedTomcatConfiguration {

    @Value("${server.additional-ports}")
    private String additionalPorts;

    @Bean
    public TomcatServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        Connector[] additionalConnectors = additionalConnector();
        if(additionalConnectors != null && additionalConnectors.length > 0) {
            tomcat.addAdditionalTomcatConnectors(additionalConnectors);
        }
        return tomcat;
    }

    private Connector[] additionalConnector() {
        if(StringUtils.isNotBlank(additionalPorts)) {
            return Arrays.stream(additionalPorts.split(","))
                .map(String::trim)
                .map(p -> {
                    Connector connector = new Connector(Http11NioProtocol.class.getCanonicalName());
                    connector.setScheme("http");
                    connector.setPort(Integer.valueOf(p));
                    return connector;
                })
                .toArray(Connector[]::new);
        }
        return null;
    }
}

In theory, I could register a custom LifecycleListener to each connector, but, as far as I know, it won't help. I've also heard something about valves, though I'm not sure how to implement them per connector.

Or maybe I'm going a completely wrong way.

I'd really appreciate any help in the matter.

akaine
  • 129
  • 9
  • Well, you most likely could add a custom valve, but I *THINK* that the port number is part of the request header. If that is the case you could do your authorization based on that port number instead of adding to the request yourself. Also, @ RequestMapping annotations can route based on header values, which means that you can also do separate authorization per mapping method using a @ PreAuthorize annotation. – hooknc Feb 11 '20 at 21:10
  • Ah, I forgot to mention that the services are exposed through a gateway under different domain names (all using 80 or 443). So the actual request won't have the correct port info. Regarding the valves: Do you know how to pass some connector identifier to a custom valve implementation? – akaine Feb 11 '20 at 22:24
  • To answer your question directly, no, I do not know how to do this work, but, yes, you should be able to create a new valve and implement the behavior that you're looking to do. A valve to look up that might be similar is the [RemoteIpValve](https://tomcat.apache.org/tomcat-9.0-doc/config/valve.html#Remote_IP_Valve). – hooknc Feb 11 '20 at 23:40

2 Answers2

1

It seems as though you have your heart set on trying a Valve, but, after some more research, I would recommend using a ServletFilter to do this work instead of a Valve.

I believe you can do this work in a Valve, but a Valve must be deployed into the tomcat/lib directory instead of being packaged inside of your application. I would urge you to consider trying to keep your application together in deployable artifact instead of having to remember to deploy one extra jar file to your tomcat instance when creating a new deployment.

From this answer, Difference between getLocalPort() and getServerPort() in servlets you should be able to access the tomcat port by calling getLocalPort() on the HttpServletRequest.

Then based on your idea in the question add a specific context name into the request header.

public class PortContextFilter implements Filter {

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {

        int localPort = request.getLocalPort();

        // if else or case statements

        ServletRequest newRequest = PortContextHttpServletRequestWrapper(request, YourPortContext)

        filterChain.doFilter(newRequest, response);
    }

    public void destroy() {
        // Nothing to do
    }

    public void init(FilterConfig filterConfig) throws ServletException {
        // Nothing to do.
    }

}

After that is done, in theory, you should be able to use the @RequestMapping annotation to route based on the name inside the header.

@PreAuthorize("hasPermission(#your_object, 'YOUR_OBJECT_WRITE')")
@RequestMapping("/yourobject/{identifier}", headers="context=<context_name>", method = RequestMethod.POST)
public String postYourObject(@PathVariable(value = "identifier") YourObject yourObject) {
    // Do something here...
    ...
}
hooknc
  • 4,854
  • 5
  • 31
  • 60
-1

One can also use RequestCondition to route requests based on port, which I think is closer to stock spring. see Spring Boot | How to dynamically add new tomcat connector?

Sam
  • 1,832
  • 19
  • 35