Simple Session-Sharing in Tomcat Cluster Using the Session-in-Cookie Pattern Part 1: The Basics

Posted by by

In a recent project we needed to deploy application changes to a Tomcat cluster without outage to the end user. To accomplish this the Tomcat sessions needed to be shared across the nodes. We opted to implement a variant of the Session-In-Cookie pattern popular in the Rails framework, a simple solution to session sharing. This blog shows how to implement this Session-In-Cookie pattern in Java.

Session Sharing Options

If sessions are not shared across cluster nodes, taking out a node will result in those sessions being lost, since they don’t exist anywhere else but in the memory of that node. If the sessions are shared in the cluster the node can be stopped without an outage to the end user, since any of the other nodes can accept the requests for these sessions and continue processing. We had a look at different options of how to share sessions between Tomcat nodes before settling on the Session-In-Cookie pattern.

Tomcat Clustering

The obvious solution is to use Tomcat´s inbuilt clustering. It enables replication of all sessions across all Tomcat instances in a cluster. All sessions exist on all nodes of the cluster.

Although this is the standard for session sharing in Tomcat clusters we didn’t go with this solution: we were concerned that the cluster was too big for replication of sessions across all nodes. Because of the expected traffic we initially had 20 nodes in the Tomcat cluster, and replicating all sessions across 20 nodes was deemed to cause too much network traffic. With deadlines looming we didn’t have time to verify this, but decided not to pursue the Tomcat clustering idea.

Session in Database

Making session data available to all nodes in a cluster can also be achieved by storing the session data in a central database. Rather than replicating the sessions to all nodes – as with the Tomcat Clustering – only one session exists in the database, and all servers access this single session. With every request the session is first retrieved from the database and placed into the local memory of the Tomcat node. Once the session is available the request is processed as usual. When the response is sent the session is written back to the database.

While this allows session sharing in a cluster it has a some drawbacks:

Two extra database calls need to be made per request: one to load the session and one to save the session

Expired sessions have to be cleaned up periodically to prevent the database from filling

The extra database calls were the reason we did not choose this solution. The performance of our application was already database-bound, which means that the database was the limiting factor for the performance of the whole application. Adding two extra calls per request would have compounded this problem even further.

External Cache

An external cache is a separate process that runs on a machine in the LAN where the session can be stored. It stores the data in memory, and allows access to the session data from all Tomcat nodes. Again there is only one copy of the session available, and all nodes access the single copy.

Examples of this architecture are memcached (open-source) and Terracotta (open-source, commercial). External caches are often used by large websites, and have proven to be reliable and fast. Despite this there was one issue significant to our situation that made us decide against the solution: There is considerable overhead of managing the external cache. It is a separate application, and administrators have to set it up, become familiar with using the cache, monitor it, and have emergency procedures in case it goes down. Finally, to avoid the session cache becoming a single point of failure, a session cache cluster or at least a fail-over node needs to be set up.

All this is cache management overhead. The important part about this management overhead was that it is not in the hands of the developers, but in the hands of a separate group: the administrators. We were looking for a solution that was in our hands, and therefore we opted against using an external session cache.

Session-In-Cookie

A simple solution for sharing sessions in a cluster is to store the session on the browser rather than on the server. This means that the session is first sent to the browser with a response, and the browser then sends it back with every request the user makes. Regardless of which node processes the request the session data is always available.

HTTP cookies are a mechanism for passing data separate to the actual content back and forth between server and browser. Storing session data in a cookie is good way to send the session to the client and pass it back to the server with the next request. The cluster node processing the request therefore has access to that session data. This pattern of storing session data in a cookie is popular in the Ruby community: the Rails framework uses it as the default session store since Rails 2.

The use of cookies for session storage is not without controversy. Critics are mostly concerned with security aspects, especially the replay attack. In this blog I will focus on the basic implementation of the pattern, and will discuss security concerns and show how to secure the cookie in part two of this blog.

There is no ready-made solution available for the Session-In-Cookie pattern in Java, so we implemented our own solution. The result was a great success, allowing our application cluster to be deployed with zero outage.

The Session-In-Cookie Implementation

To implement the pattern we need to be able to do two things

process the incoming request to deserialise cookie to session attributes

process the outgoing response to serialise the session attributes to the cookie

Our solution is uses a single Servlet filter that does both the deserialisation and the serialisation.

The Servlet Filter

The servlet filter is added to the web.xml, and listens to all page requests, in our case anything that ends with .do.

CookieSessionFilter CookieSessionFilter au.com.marcsworld.filter.CookieSessionFilter CookieSessionFilter *.do

The doFilter method of the CookieSession servlet filter is relatively straightforward:

@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { LOGGER.debug("doFilter()"); getSessionFromCookie((HttpServletRequest) request); FilterResponseWrapper responseWrapper = new FilterResponseWrapper((HttpServletResponse) response); chain.doFilter(request, responseWrapper); storeSessionInCookie((HttpServletRequest) request, responseWrapper); responseWrapper.flushBuffer(); }

Line 6 processes an incoming request to deserialise the cookie, line 9 passes processing along the filter chain to the controllers, and line 11 serialises the session back into the cookie.

Committed Response

What makes the solution a bit complicated is the fact that we cannot work directly with the response. The response has to be wrapped before it is passed to the filter chain. This has to be done because a cookie cannot be added to a response that has already been committed.

Many times a response is already committed by the time it passes back from the filter chain. StackOverflow has the explanation on when a response is committed:

A response will be committed whenever one or more of the following conditions is met:

HttpServletResponse#sendRedirect() has been called.

More than 2K has already been written to the response output, either by Servlet or JSP.

More than 0K but less than 2K has been written and flush() has been invoked on the response output stream, either by Servlet or JSP.

To be able to add the cookie in the servlet filter after processing is done the servlet response has to be wrapped to intercept any premature commit. For that we create a FilterResponseWrapper and a WrappedServletOutputStream. The FilterResponseWrapper has its own WrappedServletOutputStream and its own PrintWriter to write the content to the output stream. All this is necessary to control the time the response is committed so we can add the session cookie just before we return the response to the client. Only when responseWrapper.flushBuffer() is called in the CookieSession servlet filter the response is committed.

FilterResponseWrapper is a subclass of javax.servlet.http.HttpServletResponseWrapper, which is a convenient implementation of the HttpServletResponse to allow modification of functionality. Its methods default to calling the underlying methods of the wrapped response object, and can be overwritten to change functionality. We overwrite getOutputStream() to ensure that data is written to the internal WrappedServletOutputStream, and overwrite getWriter(), and flushBuffer() to ensure flushing is done on the internal PrintWriter.

package au.com.marcsworld.filter; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; import org.apache.log4j.Logger; public class FilterResponseWrapper extends HttpServletResponseWrapper { Logger LOGGER = Logger.getLogger(FilterResponseWrapper.class); private final WrappedServletOutputStream output; private final PrintWriter writer; public FilterResponseWrapper(HttpServletResponse response) throws IOException { super(response); output = new WrappedServletOutputStream(response.getOutputStream()); writer = new PrintWriter(output, true); } @Override public ServletOutputStream getOutputStream() throws IOException { LOGGER.debug("getOutputStream()"); return output; } @Override public PrintWriter getWriter() throws IOException { LOGGER.debug("getWriter()"); return writer; } @Override public void flushBuffer() throws IOException { LOGGER.debug("flushBuffer()"); writer.flush(); } }

The WrappedServletOutputStream extends a normal ServletOutputStream to provide an internal FilterOutputStream to control flushing the stream.

package au.com.marcsworld.filter; import java.io.FilterOutputStream; import java.io.IOException; import javax.servlet.ServletOutputStream; import org.apache.log4j.Logger; public class WrappedServletOutputStream extends ServletOutputStream { Logger LOGGER = Logger.getLogger(WrappedServletOutputStream.class); private final FilterOutputStream output; public WrappedServletOutputStream(ServletOutputStream output) { this.output = new FilterOutputStream(output); } @Override public void write(int b) throws IOException { LOGGER.debug("write()"); output.write(b); } @Override public void flush() throws IOException { LOGGER.debug("flush()"); output.flush(); } }

After all this plumbing work let’s have a look at the more interesting bits: the deserialisation and serialisation of the cookie.

Deserialisation

When a request comes in the servlet filter will try to find an existing session cookie and deserialise the cookie value into a CookieSession object.

private void getSessionFromCookie(HttpServletRequest httpServletRequest) throws IOException { LOGGER.debug("getSessionFromCookie()"); Cookie sessionCookie = getSessionCookie(httpServletRequest); // Kill existing session and get a fresh one HttpSession session = httpServletRequest.getSession(); session.invalidate(); session = httpServletRequest.getSession(); if (null != sessionCookie) { String serialisedSession = sessionCookie.getValue(); try { CookieSession cookieSession = (CookieSession) fromString(serialisedSession); Map<String, Object> attributes = cookieSession.getAttributes(); for (Map.Entry<String, Object> attribute : attributes.entrySet()) { session.setAttribute(attribute.getKey(), attribute.getValue()); } } catch (Exception e) { LOGGER.error(e.getMessage(), e); } } } private Cookie getSessionCookie(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null) for (Cookie cookie : cookies) { if ("session".equals(cookie.getName())) { return cookie; } } return null; }

Rather than storing the complete session in the cookie only the attribute map of the HTTP session is stored, because sessions are created by the container and cannot be instantiated. To get a fresh session we can add the attribute map to we first invalidate any existing session, and then have the container create a new one for us.

CookieSession is a container class for that attribute map. Once we have obtained a new session from the container the attribute map of the CookieSession object is copied over to the attribute map of the HTTP session.

package au.com.marcsworld; import java.io.Serializable; import java.util.Map; public class CookieSession implements Serializable{ private static final long serialVersionUID = -779823856918700575L; private Map<String, Object> attributes; public Map<String, Object> getAttributes() { return attributes; } public void setAttributes(Map<String, Object> attributes) { this.attributes = attributes; } }

The actual deserialisation from String to SessionCookie object is straightforward, with the exception of the base64 parsing. The cookie value is stored in Base64 (ASCII) to ensure the value only contains valid characters.

import java.io.ObjectInputStream; import java.io.ByteArrayInputStream; import javax.xml.bind; (...) private Object fromString(String string) throws IOException, ClassNotFoundException { byte[] data = DatatypeConverter.parseBase64Binary(string); ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(data)); Object object = objectInputStream.readObject(); objectInputStream.close(); return object; }

The attributes in the attribute map of the deserialised SessionCookie object are then copied over into the newly created HTTP session. After that the request and the session with the attribute map are passed down the filter chain to the controller. The controller can operate on the existing attributes in the session and modify them.

Serialisation

Once a controller is finished writing the response, the second part of the filter will serialise the modified session attribute map into the session cookie, which is then added to the response and sent back to the user.

private void storeSessionInCookie(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { LOGGER.debug("storeSessionInCookie()"); HttpSession session = httpServletRequest.getSession(false); Enumeration attributeNames = session.getAttributeNames(); Map<String, Object> attributes = new HashMap<String, Object>(); while (attributeNames.hasMoreElements()) { String attributeName = attributeNames.nextElement(); attributes.put(attributeName, session.getAttribute(attributeName)); } CookieSession cookieSession = new CookieSession(); cookieSession.setAttributes(attributes); try { String serialisedCookieSession = toString((Serializable) cookieSession); Cookie newSessionCookie = new Cookie("session", serialisedCookieSession); newSessionCookie.setPath("/"); httpServletResponse.addCookie(newSessionCookie); } catch (Exception e) { LOGGER.error(e.getMessage(), e); } }

The serialisation itself happens in the toString method, which converts the resulting String to Base64.

private String toString(Serializable object) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(object); objectOutputStream.close(); return new String(DatatypeConverter.printBase64Binary(byteArrayOutputStream.toByteArray())); }

Concerns Using Cookies

A major concern when storing the session data in a cookie is the overhead the cookie induces. Cookies are sent with each request to the server: not just requests for dynamic content, but also all requests to static content like images or css.

For that reason it is important to either keep the cookie as small as possible, or to make sure that the static content is not loaded through the application server. If the request for static content is not made to the application server directly the cookie will not be sent with the request. Serving static content via Apache or a CDN is therefore recommended.

There is another caveat with cookies: although there is no mandated limit in cookie size according to RFC 2965 “HTTP State Management Mechanism”, it is recommended not to exceed 4096 bytes per cookie. This size includes everything: name, value, expiration date etc. After some testing we concluded storing a member and a few other small attributes in the session is perfectly fine with this pattern.

Self-Containment

The code to implement the Session-In-Cookie pattern is completely contained in the CookieSessionFilter and related classes. The pattern therefore is transparent to the application developer, there is no need for special treatment when the session is stored this way. He can use the HTTP session as before without considering the implementation detail of the session storage. If another session-sharing solution like Tomcat clustering is chosen later no modification to the application code is necessary. Just take the filter out of the deployment descriptor and sessions are not stored in a cookie anymore.

Security Features

This blog only shows a base implementation of the pattern without regards to security. Even though the session in the cookie is not human-readable, it is easy for a hacker to tamper with this data. For that reason we added some security elements to this base implementation in our production version. We implemented a cookie timeout as well as encrypted and signed the cookie data before sending it to the client. The timeout limits the period a session is active and can be hijacked, and encryption and signing ensures that the data has not been tampered with. I will describe our implementation of these security features in the upcoming second part of this blog.

Conclusion

Once the filter is in place the application now can share sessions in a cluster. This solves our original problem and deploy to a Tomcat cluster without outage to the end user. This solution complemented our existing Continuous Delivery process perfectly: automated builds, unit tests, integration tests, and push-button promotion to different environments had been in place already, and we added now zero-outage deployment to the production environment. Our cluster was able to handle up to 1,000,000 requests per hour before the database couldn’t take any more, and with the Session-in-Cookie solution we could deploy new versions of our application any time, even during heavy traffic. The solution is now seen as a proof-of-concept to adopt session-sharing across other applications as well. The ability to share sessions is very powerful, and this pattern is a simple way to implement this.

The complete source code can be found on Github, or you jump to Part 2 of this blog.

