Schlagwörter
Mein aktuelles Projekt ist sehr content-lastig. Soll heißen es geht mehr darum redaktionellen Content aus dem CMS anzuzeigen bzw. Produktdaten aus dem PIM anzuzeigen als um Interaktion mit dem User. Tatsächlich gibt es so wenig Interaktion mit dem User, dass wir bisher komplett stateless laufen und auf ein Session-Handling verzichten konnten. Die Server laufen ohne zentralen Sessionstore und ohne Session-Stickyness.
In Zukunft soll das ggf. anders werden: Abhängig vom Erfolg der Seite sollen auch noch Ecommerce-Funktionen dazu kommen. Wenn das passiert, werden wir uns auch über das Session-Handling Gedanken machen müssen, das ist aber erst mal aufgeschoben. Wie so oft, hält sich das Leben wieder mal nicht an die Spielregeln, und so kommen mit dem aktuellen Sprint zwei Anforderungen, die einen State in der Applikation benötigen. Sie sind klein genug, dass es sich dafür niemals lohnt eine neue Server-Komponente als zentralen Sessionstore aufzubauen. Ich will aber auch meine stateless Server nicht aufgeben. Da die zu speichernden Informationen absolut unkritisch sind, kamen wir im Team schnell zum Ergebnis, dass Cookies das Mittel der Wahl sind.
Und dann kam eine Idee zum Einsatz, die mir von Theo Schlossnagle in seinem großartigen Buch Scalable Internet Architectures eingepflanzt wurde, und nun 5 Jahre Zeit zum Gären hatte: Um der Möglichkeit Rechnung zu tragen, dass wir ggf. in Zukunft doch unseren Sessionstore bekommen und die Infos dann in der Session anstatt in Cookies stehen können, werden wir den Abstraktionsgrad der Cookies erhöhen und auf die Informationen über die HttpSession zugreifen. Die Cookies sind für uns nur noch ein technisches Implementierungsdetail der HttpSession.
Die Implementierung erfolgt auf Basis eines eigenen Session-Handlings. Das einzige, war wir brauchen ist ein eigener ISessionPersistor, der die Session-Werte aus den Cookies liest und zum Ende der Verarbeitung wieder in die Cookies schreibt:
public class CookieSessionPersistor implements ISessionPersistor { private HttpServletRequest request; private HttpServletResponse response; private Map<String, Converter<String, ?>> converters = new HashMap<String, Converter<String,?>>(); public CookieSessionPersistor(HttpServletRequest request, HttpServletResponse response) { this.request = request; this.response = response; converters.put("lang", Converters.stringConverter); converters.put("track", Converters.booleanConverter); } @Override public Map<String, Object> getSession(String sid) { Map<String, Object> map = new HashMap<String, Object>(); for (Cookie cookie : request.getCookies()) { String key = cookie.getName(); Converter<String, ?> converter = converters.get(key); if (converter != null) { map.put(key, converter.convert(cookie.getValue())); } } return map; } @Override public void saveSession(String sid, Map<String, Object> sessionMap) { for (Entry<String, Object> entry : sessionMap.entrySet()) { if (converters.keySet().contains(entry.getKey())) { // nur vorab definierte Werte in Cookies speichern Cookie cookie = new Cookie(entry.getKey(), entry.getValue().toString()); response.addCookie(cookie); } // else { throw Something(); } } }
Der SessionPersistor ist recht straightforward. Eine Besonderheit ist die Map mit den Convertern. Diese hat zwei Bedeutungen:
- In einem Cookie können nur Strings gespeichert werden, aber irgendwie müssen die Strings in den ursprünglichen Datentypen innerhalb der Session konvertiert werden. Dies erfolgt über die zum Key definierten Converter.
- Eine HttpSession ist eigentlich eine sichere Datenquelle, in der ein Entwickler üblicherweise vertrauliche Objekte speichern und Objekte ungeprüft auslesen können. Diese haben wir nun kompromitiert. Um sicherzustellen, dass nicht ungewollt vetrauliche Informationen in einem Cookie landen, müssen alle Werte, die rausgegeben werden können, explizit im CookieSessionPersistor „freigeschaltet“ werden.
Dem aufmerksamen Leser ist aufgefallen, dass der CookieSessionPersistor einen HttpRequest und eine HttpReponse im Construktur übergeben bekommt. Das passt mit der Idee diesen als Bean im MySessionFilter zu wiren nicht zusammen. Daher müssen wir am Filter auch noch Anpassungen vornehmen:
public class MySessionFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String sid = request.getId(); ISessionPersistor sessionPersistor = new CookieSessionPersistor(request, response); Map<String, Object> sessionMap = sessionPersistor.getSession(sid); OwnSessionHttpServletRequest wrappedRequest = new OwnSessionHttpServletRequest(request, sid, sessionMap); try { chain.doFilter(wrappedRequest, response); } finally { sessionPersistor.saveSession(sid, sessionMap); } } }
Fazit
Mit ein paar Codezeilen haben wir es geschafft verteilten Sessionstore zu implementieren. Bei Bedarf kann die Lebensdauer der Objekte über das Sessionende hinaus erhöht werden, dazu muss das Expiry-Date des Cookies entsprechend erhöht werden.
In meiner Einleitung tu ich diesem Pattern sogar unrecht, da es wie eine aus der Not heraus geborene Idee erscheint. In Wirklichkeit haben wir einen hochperformanten, skalierbaren und stark verteilten Sessionstore eingerichtet, der quasi mit beliebiger Last umgehen kann.
Man kann dieses Pattern noch weiterspinnen und sich entscheiden die Map z.B. in JSON zu serialisieren und dann ins Cookie zu schreiben. Um zu verhindern, dass User die Werte manipulieren, können diese noch verschlüsselt und mit einem Hash abgesichert werden.
Zwei Haken werden wir aber nicht wegdiskutiert bekommen:
- Die zu speichernde Datenmenge ist begrenzt => Wir wollen keine riesigen Cookies durch die Welt versenden.
- Selbst wenn wir die Cookies verschlüsseln, hashen und durch den Fleischwolf drehen, bleibt es dabei, dass ein User diese hacken und uns kompromierte Werte unterschieben könnte. Für den Hochsicherheitsbereich ist das sicher kein geeignetes Pattern.
Überall dort, wor wir mit diesen Einschränkungen leben können, können wir mit ganz wenig Aufwand eine hochskalierbare Lösung fürs Sessionhandling implementieren.