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:
<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 |