We recently configured an Apache httpd server to provide load balancing to three instances of Tomcat using mod_proxy_balancer over AJP. The backend application is not "distributable" (the sessions are not Serializable yet), so it’s not a proper cluster. Therefore, the sessions must be "sticky" using the JSESSIONID and jvmRoute in Tomcat. However, the application must be well-behaved regarding session creation to get full value from the balancer.

Load Balanced to Tomcat

The basic configuration looks like:

loadbalancer.conf
<Proxy balancer://app-cluster>
        BalancerMember ajp://app-01:8009 route=app01
        BalancerMember ajp://app-02:8009 route=app02
        BalancerMember ajp://app-03:8009 route=app03
</Proxy>

ProxyPass /app balancer://app-cluster/app stickysession=JSESSIONID nofailover=On

This setup works really well. New sessions are balanced across the server and existing sessions are sent to the backend that created the session.

However, minor difficulties arise if your application is not well-behaved when a user logs or times out. If, on logout, the server invalidates the current session, but creates a new one while rendering the logout response, Apache will see the new cookie on the next request. Apache proxy_balancer only looks at the jvmRoute (not the entire session id). Therefore, from Apache’s point of view, nothing has changed so the new session is forced to the same backend.

This reduces the effectiveness of the balancing over time because users will get "stuck" on a server. Further, if you use the balancer-manager’s drain function, users who timeout or logout and re-connect will get reconnected to the same backend. Thus, draining a backend of sessions may take much longer than necessary.

To get best load balancing, at least one request must exclude the JSESSIONID before (or when) the new session is created. So long as Apache sees a request without a session cookie, it will have an opportunity to balance the session as if it were new.

Logout with Spring Security

Spring Security can handle the logout with just a little encouragement:

    http.logout()
        .deleteCookies('JSESSIONID')
        .logoutSuccessUrl('/loggedout')
        .permitAll()

The logoutSessessUrl() is rendered (not redirected) therefore it must not create a new session. However, it could redirect to a page which creates a new session.

Invalid (timed out) Session

For invalid sessions, the Spring Security provides invalidSessionUrl(). Configured like:

java
    http.sessionManagement()
    .invalidSessionUrl('/timedout')

This configures a SimpleRedirectInvalidSessionStrategy which re-directs to the specified URL. However, this implementation is unsatisfactory. It defaults to creating a new session and does not delete the existing cookie.

To resolve this, we need to replace the InvalidSessionStrategy with one that meets the requirements. The API for this is not exposed directly, so a ObjectPostProcessor for SessionManagementFilter is in order:

    http.addObjectPostProcessor(new InvalidSessionPostProcessor())

    ...

static class InvalidSessionPostProcessor implements ObjectPostProcessor<SessionManagementFilter> {

    Object postProcess(Object filter) {
        filter.setInvalidSessionStrategy(
           new ClearInvalidSessionStrategyDecorator(new SimpleRedirectInvalidSessionStrategy('/timedout'))
        )
        return filter
    }
}

static class ClearInvalidSessionStrategyDecorator implements InvalidSessionStrategy {

    SimpleRedirectInvalidSessionStrategy wrapped

    ClearInvalidSessionStrategyDecorator(SimpleRedirectInvalidSessionStrategy strategy) {
        wrapped = strategy
        wrapped.setCreateNewSession(false)
    }

    void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {

        def session = request.getCookies().find { it.name == 'JSESSIONID' }
        if (session) {
            def cookie = new Cookie(session.name, null)
            cookie.path = request.contextPath ?: '/'
            cookie.maxAge = 0
            response.addCookie(cookie)
        }
        wrapped.onInvalidSessionDetected(request,response)
    }
}

The decorater just ensures that new session are not created and delete’s the existing cookie. I would have preferred to decorate the existing strategy from the filter (to use it "as configured"). But, alas, the `invalidSessionStrategy' is a write-only property.

In this case, the browser is redirected. Therefore, the /timedout can create a new session. That is, you could redirect to Home or a login page immediately.

Important
The code examples above are Groovy. Add semicolons and boilerplate as needed…​
Note

EDIT: The first version of this post attempted to expire the cookie invoking setMaxAge(0) on the existing cookie from the request and adding it back to the response. That worked on Chrome but not FireFox. The Cookie from the request did not have a path value. Chrome and FireFox both record the servlet context as the path (as shown by the browser’s developer tools). Apparenlty, Chrome will allow you to expire the cookie without specifying the path, while FireFox requires it. In my haste, I tested only in Chrome. The revised code above is derived from Spring Security’s CookieClearingLogoutHandler (and Groovy-fied).