4
votes

I'm running a Spring Boot 1.2.3 application with embedded Tomcat.

I'd like to inject a custom contextPath on every request, based on the first part of the URL.

Examples:

  1. http://localhost:8080/foo has by default contextPath="" and should get contextPath="foo"

  2. http://localhost:8080/foo/bar has by default contextPath="" and should get contextPath="foo"

(URLs without path should stay as is)

I tried to write a custom javax.servlet.Filter with @Order(Ordered.HIGHEST_PRECEDENCE), but it seems like I'm missing something. Here's the code:

@Component @Order(Ordered.HIGHEST_PRECEDENCE)
public class MultiTenancyFilter implements Filter {
    private final static Pattern pattern = Pattern.compile("^/(?<contextpath>[^/]+).*$");

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        final HttpServletRequest req = (HttpServletRequest) request;
        final String requestURI = req.getRequestURI();

        Matcher matcher = pattern.matcher(requestURI);
        if(matcher.matches()) {
            chain.doFilter(new HttpServletRequestWrapper(req) {
                @Override
                public String getContextPath() {
                    return "/"+matcher.group("contextpath");
                }
            }, response);
        }
    }

    @Override public void init(FilterConfig filterConfig) throws ServletException {}
    @Override public void destroy() {}
}

This should simply take the String after the first / and before (if any) the second / and then use it as return value for getContextPath().


But Spring @Controller @RequestMapping and Spring Security's antMatchers("/") does not seem to respect it. Both still work as if contextPath="".


How can I dynamically override the context path for each request?

1

1 Answers

3
votes

Got it working!

Spring Security docs ( http://docs.spring.io/spring-security/site/docs/3.1.x/reference/security-filter-chain.html ) say: "Spring Security is only interested in securing paths within the application, so the contextPath is ignored. Unfortunately, the servlet spec does not define exactly what the values of servletPath and pathInfo will contain for a particular request URI. [...] The strategy is implemented in the class AntPathRequestMatcher which uses Spring's AntPathMatcher to perform a case-insensitive match of the pattern against the concatenated servletPath and pathInfo, ignoring the queryString."

So I just did override servletPath and contextPath (even if it's not used by Spring Security). Additionally I added some small redirect, because normally when hitting http://localhost:8080/myContext you get redirected to http://localhost:8080/myContext/ and Spring Securities Ant Matcher did not like the missing trailing slash.

So here's my MultiTenancyFilter code:

@Component @Order(Ordered.HIGHEST_PRECEDENCE)
public class MultiTenancyFilter extends OncePerRequestFilter {
    private final static Pattern pattern = Pattern.compile("^(?<contextPath>/[^/]+)(?<servletPath>.*)$");

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Matcher matcher = pattern.matcher(request.getServletPath());
        if(matcher.matches()) {
            final String contextPath = matcher.group("contextPath");
            final String servletPath = matcher.group("servletPath");

            if(servletPath.trim().isEmpty()) {
                response.sendRedirect(contextPath+"/");
                return;
            }

            filterChain.doFilter(new HttpServletRequestWrapper(request) {
                @Override
                public String getContextPath() {
                    return contextPath;
                }
                @Override
                public String getServletPath() {
                    return servletPath;
                }
            }, response);
        } else {
            filterChain.doFilter(request, response);
        }
    }

    @Override
    protected String getAlreadyFilteredAttributeName() {
        return "multiTenancyFilter" + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX;
    }
}

It simply extracts the contextPath and servletPath using the URL schema mentioned here: https://theholyjava.wordpress.com/2014/03/24/httpservletrequest-requesturirequesturlcontextpathservletpathpathinfoquerystring/

Additionally I had to provide a custom getAlreadyFilteredAttributeName method, because else the filter got called twice. (This resulted in stripping the contextPath twice)