<jsptutorial />

Session-Handling, Cookies und URL-Rewriting


Wozu dienen Sessions

Im Kapitel Client-Anfragen haben wir gezeigt, wie mittels HTTP Anfrageparameter mitgegeben und in unseren Java-Anwendungen ausgewertet werden können. Dabei handelt es sich aber stets nur um Anfrage-Werte einer Seite. Von Anfrage zu Anfrage werden die Werte ohne besondere Vorkehrungen wieder vergessen. Das HTTP-Protokoll zwischen Webserver und Browser sieht von sich aus keinen Zustand vor.
Dennoch benötigen wir einen Zustand über mehrere Anfragen hinweg. Bei einem Webmailer bspw. loggt sich die Nutzerin einmal ein. Klickt sie im weiteren Verlauf auf eine Mail, um sich diese anzusehen, so werden ihre Logindaten natürlich nicht mehr abgefragt. Sie werden aber auch nicht mit dem Formular oder den Links mitgeschickt. D.h. der Server kann erkennen, dass die Anfrage zu der vorherigen gehört, dass beide Anfragen (und weitere folgende) eine gemeinsame Nutzungssitzung, die Session, bilden.
Das Session-Konzept sieht zum einen ein Erkennungsmerkmal vor, anhand dessen der Webserver trotz des statuslosen Protokolls HTTP erkennen kann, dass mehrere aufeinanderfolgende Requests zu einer Session gehören. Zum anderen ist integraler Bestandteil dieses Konzepts, dass eine Session einen Speicherbereich nutzt, auf den alle Anfragen eben dieser Session zugreifen können, von dem aber Anfragen, die zu anderen Sessions gehören, ausgeschlossen sind. Im Beispiel des Webmailers könnten in der Session also bspw. die Verbindungsdaten dieser Nutzerin zu ihrem Mail-Provider gespeichert werden. Ebenso, welcher Ordner gerade geöffnet ist, bei mehreren Mailkonten auch, welches dieser Konten gerade aktuell verwaltet wird usw.

Zum SeitenanfangZum Seitenanfang

Mögliche Unterscheidungsmerkmale für Sessions

Da das HTTP-Protokoll von sich aus keine Möglichkeit anbietet, zu erkennen, ob mehrere Anfragen zu einer Session gehören, muss man sich anderweitig behelfen. Dabei gibt es vier mögliche Lösungen:


  1. Eine Session-ID wird stets als URL- bzw. Form-Parameter mitgegeben.
    Diesen Weg hat sehr lange Cold Fusion verfolgt, wo früher die zwei Formularwerte CFID und CFTOKEN genutzt wurden, um eine Session zu kennzeichnen.
    Dieser Weg wird von der Servlet-Spezifikation nicht unterstützt und soll daher hier nicht weiter betrachtet werden.

  2. Eine Session-ID wird per Cookie übertragen
    Dies ist die bei weitem geläufigste Methode. Dazu sendet der Server bei der ersten Anfrage einen Set-Cookie-Header mit, der eine eindeutige ID enthält. Der Browser sendet danach mit jedem Request einen Cookie-Header an den Ursprungsserver mit, der den zuvor gesetzten Wert enthält. Sogenannte Session-Cookies ("nonpersistent cookies") werden mit dem Schließen des Browsers gelöscht. Es gibt aber auch langlebige Cookies ("persistent cookies"), die von der Anwendung gesetzt werden, um bspw. der Nutzerin beim nächsten Besuch der Seite sofort einen personalisierten Inhalt anzeigen zu können (Amazon verwendet bspw. diese Cookie-Nutzung). Diese langlebigen Cookies interessieren in diesem Kapitel nicht.

  3. Die Session-ID wird mittels URL-Rewriting weiter gegeben
    Als eine dritte Möglichkeit kann die Session-ID auch in die URL kodiert werden. Bspw. kann man URLs in der folgenden Art erzeugen:

    http://www.mycompany.com/;jsessionid=someUniqueSessionID?param1=value1

    Dabei wird die Session-ID per Semikolon vom Pfad der Ressource abgetrennt und vor den Request-Parametern eingefügt.1 Zur Erzeugung geeigneter URLs sieht die Servlet-Spezifikation einige Methoden des HttpServletResponse-Objekts vor. Dazu mehr im Abschnitt zum URL-Rewriting.

  4. Bei sicheren Websites wird die SSL-Verbindung zum Session-Handling genutzt
    Nutzt man SSL (HTTPS) zur Verbindung zum Server, so tauschen Client und Server beim Aufbau der Verbindung einen gemeinsamen Schlüssel aus. Dieser gemeinsame Schlüssel wird anschließend zum Verschlüsseln und Entschlüsseln jedes Nutzdatentransfers (also der Requests und der Antworten darauf) genutzt. Die Servlet-Spezifikation benennt als eine Möglichkeit des Session-Trackings die Verwendung dieses SSL-Keys. De facto werden von den Container-Herstellern aber nur die beiden verpflichtend zu unterstützenden Methoden URL-Rewriting und Cookies angeboten, weswegen wir uns im Folgenden auch nur mit diesen beiden Methoden näher beschäftigen.


Zum SeitenanfangZum Seitenanfang

Cookies

Dies ist das Standard-Verfahren zum Verfolgen von Webanwendungs-Sitzungen. Für die Anwendungsentwicklerin ist dies die bequemste Art zur Sitzungsverfolgung. Solange Cookies nicht mit einer der unten angegeben Methoden unterbunden werden, sendet der Server bei der ersten Anfrage der Nutzerin einen Set-Cookie-Header, dessen Wert den Namen des Cookies und den eigentlichen Cookie-Wert ergänzt um ein paar hier nicht interessierende Angaben enthält.2 Gemäß Servlet-Spezifikation muss der Cookie-Name für die Session-ID immer JSESSIONID lauten.
Der Browser wertet diese Information aus und sendet bei weiteren Anfragen an diesen Server das zuvor empfangene Cookie mit. Anhand dessen erkennt der Server, zu welcher Session die Anfrage gehört. Weder muss die Administratorin spezielle Konfigurationen vornehmen, noch muss die Anwendungsentwicklerin die Cookies in ihrem Programm explizit setzen. Die Servlet-Spezifikation garantiert, dass alle Servlet-Container default-mäßig Cookies unterstützen und automatisch zum Session-Handling nutzen.
Zur Verdeutlichung dessen, was auf HTTP-Ebene passiert, setzen wir zwei Anfragen per Telnet ab:

centipede> $ telnet localhost 8080
Trying 127.0.0.1...
Connected to centipede.
Escape character is '^]'.
GET /examples/index.jsp HTTP/1.1
Host: localhost

HTTP/1.1 200 OK
X-Powered-By: JSP/2.1
Set-Cookie: JSESSIONID=03f52fcdccdfd070be0cbc8dd82f; Path=/jsp21_examples
Server: Sun Java System Application Server Enterprise Edition 9.1
Content-Type: text/html;charset=UTF-8
Content-Length: 327
Date: Thu, 17 May 2007 13:29:20 GMT

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Simple ession example</title>
    </head>
    <body>
       <h2>Simple session example</h2>
    </body>
</html>


centipede> $ telnet localhost 8080
Trying 127.0.0.1...
Connected to centipede.
Escape character is '^]'.
GET /examples/index.jsp HTTP/1.1
Host: localhost
Cookie: JSESSIONID=03f52fcdccdfd070be0cbc8dd82f

HTTP/1.1 200 OK
X-Powered-By: JSP/2.1
Server: Sun Java System Application Server Enterprise Edition 9.1
Content-Type: text/html;charset=UTF-8
Content-Length: 355
Date: Thu, 17 May 2007 13:31:31 GMT

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Simple ession example</title>
    </head>
    <body>
       <h2>Simple session example</h2>
       Thu May 17 14:29:20 CET 2007
    </body>
</html>

Zu beachten ist der Unterschied zwischen beiden: Bei der Antwort des Servers auf die erste Anfrage eines Browsers an das System wird der Cookie im Response-Header "Set-Cookie" gesetzt. Bei der zweiten wird das Cookie vom Browser im Request-Header "Cookie" an den Server zurück gesendet. Die Antwort auf die erste Anfrage enthält mit Ausnahme der Überschrift keine Ausgabe. Die Antwort auf die Folgeanfragen, bei denen das Cookie vom Browser mitgesendet wird, enthält zudem das Datum, an dem zuletzt auf diese Seite zugegriffen wurde. Dieser Wert wurde in der Session gespeichert und ist somit von Nutzerin zu Nutzerin unterschiedlich.
So einfach die Handhabung von Cookies für die Anwendungsentwicklerin und die Nutzerin sind, so gibt es dennoch einige Nachteile. Cookies werden von Werbeanbietern genutzt, um Nutzungsprofile über Nutzerinnen diverser Dienste anzulegen. Dies ist leider eine weit verbreitete Praxis. Viele Webseiten nutzen Werbeanbieter wie DoubleClick, RealMedia, InsightExpress, Omniture und diverse andere, um auf ihren Seiten Werbeeinnahmen zu generieren. Dazu werden bspw. Banner von den Servern der Anbieter geladen und diese setzen ein Cookie. Besucht man nun eine andere Seite, die ebenfalls ein Banner von diesem Anbieter nutzt, so wird das zuvor angelegte Cookie an den Server gesendet, da das Banner bei beiden Besuchen vom selben Webserver, dem Server des Werbeanbieters kommt. Anhand dieser Daten können diese Anbieter umfangreiche Informationen über die Nutzerinnen von Seiten an die Werbekunden liefern. Dies führt, neben den in Zusammenhang mit Cookies auftretenden Sicherheitsproblemen (häufig über XSS-Atacken) dazu, dass etliche Nutzerinnen Cookies generell ausschalten oder gezielt, je nach Webseite, blockieren.
Aufgrund der Nachteile, die mit Cookies verbunden sind, ziehen manche Webseiten-Betreiberinnen - wie auch wir - vor, erst gar keine Cookies zu setzen. Entsprechend sehen eigentlich alle Container vor, dass man das Setzen von Cookies zum Erkennen der Session durch den Servlet-Container unterbinden kann. Zu beachten ist, dass diese Einstellungen nicht das manuelle Setzen von Cookies im Programmcode unterbinden. Dieses ist auch mit diesen Einstellungen weiterhin möglich.

Cookies in Glassfish ausschalten

Die Einstellung für Cookies wird beim Applikationsserver GlassFish für jede Webanwendung konfiguriert. Somit ist es möglich, Anwendungen mit Cookies neben solchen laufen zu lassen, die ausschließlich auf URL-Rewriting setzen. Es ist allerdings nicht möglich, an einer Stelle für alle Anwendungen des Servers zentral Cookies zu unterbinden.
Wie alle speziellen Werte für eine Webanwendung, die nicht bereits durch den Deployment Deskriptor der Spezifikation geregelt sind, wird bei Glassfish diese Einstellung in der Datei sun-web.xml vorgenommen. Diese Datei wird direkt unterhalb des Verzeichnisses WEB-INF der Web-Anwendung und somit im gleichen Verzeichnis wie der Deployment Deskriptor abgelegt.
Das folgende XML-Fragment zeigt den Minimal-Eintrag, der in der sun-web.xml notwendig ist, um das Setzen von Cookies zu verhindern:
<sun-web-app>
   <!-- ... some other configuration ... -->
   <session-config>
      <session-properties>
         <property name="enableCookies" value="false" />     <!-- default value is true -->
         <property name="enableURLRewriting" value="true" /> <!-- default value is true -->
      </session-properties>
   </session-config>
</sun-web-app>

Cookies im Tomcat ausschalten

Das Vorgehen beim Tomcat ist ähnlich dem von Glassfish. Tomcat nutzt das Konzept von Kontexten, die Webanwendungen entsprechen. Für jeden Kontext kann das Session-Handling individuell gesetzt werden. Im Unterschied zu Glassfish existiert beim Tomcat allerdings ein Default-Kontext, mit dem es möglich ist, einen Default-Eintrag vorzunehmen und doch für jede Anwendung (genauer jeden Kontext) eine individuell abweichende Konfiguration zu erlauben. Der Eintrag ist im Context-Element der Webanwendung vorzunehmen.
Dieses Context-Element kann man in der $CATALINA_HOME/conf/server.xml-Datei vornehmen, was aber seit dem Tomcat 5.0 nicht mehr empfohlen wird. Alternativ kann man auch eine context.xml-Datei, bzw. eine $webapp_name.xml-Datei an geeignete Stellen im Tomcat-Verzeichnisbaum ablegen. Schlussendlich kann man eine Datei, die dann context.xml heißen muss, im Verzeichnis /META-INF unterhalb des Wurzelverzeichnisses der Webanwendung ablegen. Die Groß-/Kleinschreibung ist hier entscheidend. Wir gehen i.F. davon aus, dass von der letzten Möglichkeit Gebrauch gemacht wird, letzlich ist es aber nicht ausschlaggebend, für welche der Möglichkeiten man sich entscheidet.3
Innerhalb des Context-Elementes muss man dann nur noch das Attribut cookies auf false setzen, wie im folgenden Context-Eintrag der Beispielanwendung gezeigt.
<Context path="firstExample" cookies="false" />

Zum SeitenanfangZum Seitenanfang

URL-Rewriting

URL-Rewriting bedeutet für die Anwendungsentwicklerin einen deutlichen Mehraufwand. Jede URL muss so umgeformt werden, dass der Server die Session-Kennung erkennt. Dazu gibt es in der Klasse javax.servlet.http.HttpServletResponse die Methoden encodeRedirectURL(String) und encodeURL(String). Diese nehmen jeweils eine URL als String entgegen und wandeln diese so um, dass eine Anfrage damit als Session der derzeitigen Nutzerin erkannt wird. Die Regeln der Umwandlung muss man daher als Entwicklerin nicht kennen. Diese Methoden wird man zumeist in Servlets oder in von diesen aufgerufenen Klassen nutzen. Zur Nutzung in JSPs selbst gibt es JSTL-Tags (s. dazu auch das Kapitel JSTL), die ebenfalls URLs umwandeln können.
Wir zeigen ein Beispiel, wie man den <c:url>-Tag nutzen kann, um die URL

http://www.jsptutorial.org/sessionHandling?lang=de

entsprechend in einer JSP zu kodieren:

<c:url var="sessionHandlingUrl" value="http://www.jsptutorial.org/sessionHandling">
   <c:param name="lang" value="de" />
</c:url>
<a href="${sessionHandlingUrl}">Session-Handling</a>

Sendet der Browser ein Cookie mit, so geben sowohl der Tag als auch die beiden encodeURL-Methoden die URL unverändert zurück. D.h. URL-Rewriting wird nur angewendet, wenn der Client kein Cookie sendet.4 Außerdem achten beide angebotenen Hilfsmittel darauf, dass URL-Rewriting nur bei Links verwendet wird, die wieder gegen den Ursprungsserver gehen. Dies ist eine wichtige Sicherheitsvorkehrung, würde man doch sonst seine Session-Kennung Links beifügen, die diese nicht nur nicht benötigen, sondern besser auch gar nicht haben sollten. Mehr zur Sicherheitsproblematik bei Sessions etwas weiter unten.

Zum SeitenanfangZum Seitenanfang

Die gemeinsame Nutzung von Daten

Nachdem wir uns bisher mit den technischen Voraussetzungen und Hintergründen beschäftigt haben, wenden wir uns nun der Nutzung einer Session für das Halten eines Zustandes zu.
Zentral für die Verwaltung der Session ist ein Objekt vom Typ javax.servlet.http.HttpSession. Dieses Objekt enthält Informationen über die Session, und es ist möglich, in diesem Objekt beliebige von java.lang.Object abgeleitete Werte abzulegen, um in weiteren Requests darauf zurückgreifen zu können. Eine Übersicht aller Methoden des Session-Objektes haben wir bereits im Kapitel Implizite Objekte gegeben. Daher wird in diesem Kapitel nur kurz das allgemeine Vorgehen im Umgang mit Sessions beschrieben. Als erstes zeigen wir jedoch, wie man ein Objekt der Klasse HttpSession erlangt.
Dazu ruft man eine der beiden Methoden getSession() oder getSession(boolean) des javax.servlet.http.HttpServletRequest-Objekts auf. Die erste Methode delegiert die Anfrage an die zweite in der Form von getSession(true). Mit dem boolschen Argument der zweiten Methode definiert man, ob eine neue Session für die Besucherin erzeugt werden soll. Ist der boolsche Parameter false und existiert für die Besucherin noch keine Session, so wird null zurück gegeben. Anhand dessen kann man prüfen, ob zunächst Initialisierungen notwendig sind oder ob die konkrete Anfrage in dieser Form überhaupt erlaubt ist, ohne an einer existierenden Session beteiligt zu sein etc.
Vergleichbar damit ist die Methode isNew() des HttpSession-Objekts. Diese Methode gibt zurück, ob die Anfrage Bestandteil einer existenten Session ist, oder die Anfrage den Anfang einer Session bildet. Diese Methode gibt aber während eines gesamten Requests immer den gleichen Wert zurück, unabhängig davon, zu welchem Zeitpunkt innerhalb der Requestverarbeitung die Session vom HttpServletRequest-Objekt geholt wurde. Bei einem garantierten Einstiegspunkt, an dem die Session erzeugt wird, ist es letztlich egal, welche der beiden Vorgehensweisen man nutzt.
Hat man ein HttpSession-Objekt, so ist die Nutzung in der Folge vor allem durch das Setzen und Auslesen von Attributen gekennzeichnet. Mit der Methode setAttribute(String, Object) kann man unter einem beliebigen Namen Objekte in die Session legen. Bei Anwendungen, die als distributed im Deployment-Deskriptor deklariert sind, die also für geclusterte Umgebungen vorgesehen sind, ist seitens der Servlet-Spezifikation nur garantiert, dass serialisierbare Objekte in die Session gelegt werden können. Alle anderen können vom Container akzeptiert werden, es ist aber nicht verpflichtend. Akzeptiert ein Container ein nicht-serialisierbares Objekt nicht, so muss er java.lang.IllegalArgumentException werfen. In nicht-geclusterten Umgebungen entfällt diese Beschränkung.
Die in der Session abgelegten Objekte kann man zu einem späteren Zeitpunkt mit getAttribute(String) unter dem verwendeten Namen wieder abrufen. Da der Rückgabewert vom Typ java.lang.Object ist, muss man das ausgelesene Objekt dabei auf den geeigneten Typen casten.
In unserem Vokabel-Beispiel legen wir ein Objekt vom Typ org.jsptutorial.examples.firstexample.domain.LearningSession in der Session ab und holen uns dieses in folgenden Requests wieder von der Session ab. Die Lernsitzung wiederum hält eine Liste der gegebenen Antworten, den Zeitpunkt des Sitzungsstarts, welche Lektion ausgewählt ist, usw.

// from file SelectUnitHandler.java:
LearningSession learningSession = new LearningSession(unit, startDate);
session.setAttribute("learningSession", learningSession);


// from file VocabAnswerHandler.java:
LearningSession learningSession = (LearningSession)session.getAttribute("learningSession");

Initialisiert wird die Session im org.jsptutorial.examples.firstexample.handlers.StartupHandler.
// do we have an old session?
HttpSession session = request.getSession(false);
if (session != null) {
   // prevent session fixation
   session.invalidate();
}
// create a new session and initialize it
session = request.getSession(true);
// add the locale setting for the used JSTL tags...
// request.getLocale() returns the locale specified as the preferred
// locale in the header section of the HTTP request generated by the
// browser on behalf of the user, so we simply use this value
Locale locale = request.getLocale();
if (locale == null) {
   locale = Locale.ENGLISH;
}
Config.set(session, Config.FMT_LOCALE, locale);

// also add the appropriate ResourceBundle to the session attribute
ResourceBundle rb = ResourceBundle.getBundle(Constants.RESOURCE_BUNDLE, locale);
session.setAttribute(Constants.ATTRIBUTE_RESOURCE_BUNDLE, rb);

Will man eine Session beenden (bspw. nachdem die Nutzerin auf Logout geklickt hat), kann und sollte man durch Aufruf der Methode invalidate() auf dem Session-Objekt alle Daten aus der Session entfernen und diese löschen.

Zum SeitenanfangZum Seitenanfang

Umgang mit Sessions

Im Umgang mit Sessions ist das Ablegen von Informationen ein Vorgang, mit dem man behutsam vorgehen muss. Das größte Problem ist, dass häufig zu viele Daten in der Session abgelegt werden. Da eine Session über die gesamte Dauer der Nutzung einer Seite durch die Anwenderin im Speicher gehalten wird, kann der Speicherverbrauch einer gut besuchten Seite bei vielen in der Session gehaltenen Daten schnell zu einem begrenzenden Faktor werden.
Werden zu viele Daten gehalten, wird zudem der Datenverkehr in einer geclusterten Webanwendung stark ansteigen, so lange man ein Session-Failover - also die automatische Übernahme einer Session von einem anderen nicht mehr erreichbaren Knoten - anstrebt. In diesem Fall müssen die am Cluster beteiligten Knoten die in den Sessions gehaltenen Daten nach jeder Änderung an diesen untereinander abgleichen. Es ist offensichtlich, dass dies bei zu vielen Session-Daten zu einem Engpass bei der Netzwerklast führt.
Aufgrund des bisher über den Speicherberbrauch Gesagten dürfte klar sein, dass man bei Anwendungen, die Gebrauch von einer Session machen, möglichst einen Logout-Button zur Verfügung stellen sollte. Zwar werden nie alle Nutzerinnen sich explizit ausloggen, dennoch hilft das gezielte Beenden der Sessions wertvolle - und vor allem nutzlos gebundene - Ressourcen einzusparen.5 Ein weiterer Nutzen ist, dass dadurch das Zeitfenster für mögliche Session-Attacken so klein wie möglich gehalten wird, dazu mehr im Abschnitt über Sicherheitsprobleme im Zusammenhang mit Sessions. Von Methoden, eine Session künstlich am Leben zu halten (bspw. über Ajax-Heartbeats) oder den Session-Timeout auf mehr als eine halbe Stunde zu setzen, raten wir ausdrücklich ab.6
Ein weiteres Problem mit Sessions ist, dass es leicht zu Inkonsistenzen kommen kann, wenn die Nutzerin bspw. mehrere Browserfenster oder -tabs verwendet. Öffnet die Nutzerin mehrere Fenster einer Anwendung mit der gleichen Session-ID über den gleichen Link, dann zeigen unmittelbar nach dem Öffnen noch beide Fenster den gleichen Zustand an. Sobald man nun in einem der Fenster eine Aktion anstößt, ist der wieder gegebene Zustand des anderen Fensters nicht mehr korrekt. Je nach Anwendung kann dies ohne besondere Vorkehrungen zu inkonsistenen Datenbeständen (falls ein Formular in den jeweiligen Fenstern mit verschiedenen Werten gefüllt wird) oder zu einem sprunghaftem Anwendungsablauf (wenn die nächste Maske sich nicht direkt aus einem Link, sondern aus dem aktuellen Zustand der Anwendung ergibt) führen. Diesem Effekt kann man mit sogenannten Session-Tokens (s. nächster Abschnitt) vorbeugen.

Zum SeitenanfangZum Seitenanfang

Session-Token

Wie beschrieben, kann es durch zwei zueinander inkonsistente Fenster zu unerwünschten Seiteneffekten kommen. Dies lässt sich aufgrund der genutzten Technologien - anders als bei Desktopanwendungen - nicht verhindern. Man kann allerdings dafür sorgen, das nur ein definierter Pfad zugelassen wird. Dazu erzeugt man bei jedem gültigen Seiten-Aufbau ein Zufalls-Token (häufig auch Nonce genannt), das bei Folge-Requests mitgegeben werden muss (bspw. als verstecktes Formular-Element oder in der Anfrage-URL). Das Token legt man zudem in der Session ab. Kommt nun eine neue Anfrage herein, so kann man das mitgesendete Token mit dem vergleichen, das in der Session abgelegt wurde. Stimmen beide nicht überein, so stammt die Anfrage von einer veralteten Seite. Dieses Verfahren ist recht verbreitet und wird bspw. von Struts oder Spring Webflow unterstützt.
In unserer Beispielanwendung nutzen wir zur Veranschaulichung ebenfalls Token, auch wenn für diese Anwendung ein Token-Handling etwas übertrieben wirkt. Dazu gibt es eine Hilfsklasse, die das Handling der Token in den Handlern vereinfacht (org.jsptutorial.examples.firstexample.handlers.util.TokenUtils).
Die einzelnen Handler entscheiden, inwieweit für sie ein Token-Handling notwendig ist. Der StartUpHandler bspw. überprüft das Token nicht, da man jederzeit die Anwendung wieder neu starten können soll. Hingegen prüft der VocabAnswerHandler das Token ab und weist ggf. die Anfrage zurück:

// validate the token
String token = request.getParameter(Constants.ATTRIBUTE_TOKEN);
if (!TokenUtils.isValid(request, token)) {
   RequestDispatcher dispatcher = request.getRequestDispatcher("/invalidState.jsp");
   dispatcher.forward(request, response);
   return null;
}

Aufgrund des bewusst unterschiedlichen Verhaltens je nach Anfrage kann das Token-Handling nicht zentral im FrontController vorgenommen werden, sondern ist Aufgabe der Handler. Alle Handler legen allerdings einheitlich am Ende ein Token in der Session ab und überlassen es den Folge-Handlern, ob sie dieses auswerten oder nicht:
// part of all Handlers:
TokenUtils.addToken(request);


// excerpt taken from TokenUtils.java:
public static void addToken(HttpServletRequest request) {
   HttpSession session = getSession(request);
   String token = getToken();
   session.setAttribute(Constants.ATTRIBUTE_TOKEN, token);
   request.setAttribute(Constants.ATTRIBUTE_TOKEN, token);
}

Wie man sieht, wird das Token gleichermaßen in den Request und in die Session gelegt. Warum nicht nur an einer Stelle? In der JSP müssen wir das Token als Bestandteil sämtlicher Links und/oder als Action-Attribut eines Formulars ausgeben. Zum Zeitpunkt der Ausgabe in der JSP kann bereits durch eine parallele Anfrage ein anderes Token als ursprünglich angefordert in der Session abgelegt worden sein könnte. Zwei Seiten würden dann das gleiche Token verwenden - etwas, was unser Token-Handling unnütz machen würde, da Anfragen beider Seiten in der Folge akzeptiert würden. Indes steht auch bei mehreren gleichzeitigen Anfragen für jede Anfrage ein eigenes Request-Objekt zur Verfügung, und somit greift die Ausgabe in der JSP weiterhin auf das korrekte Token zu, wenn sie dieses aus dem Request nimmt.

Zum SeitenanfangZum Seitenanfang

Timeout-Handling

Im Abschnitt "Umgang mit Session" haben wir beschrieben, dass man eine Session zeitlich begrenzen sollte, um zu verhindern, dass zu viele unnütze Daten für eine Nutzerin vorgehalten werden. Wann allerdings eine Nutzerin ihre Sitzung beendet hat und somit die gehaltenen Daten unnütz geworden sind, ist aufgrund des statuslosen HTTP-Protokolls nicht leicht zu erkennen.
Der einfachste Fall ist natürlich, dass die Nutzerin sich von der Anwendung abmeldet. In dem Fall werden etwaige Session-Daten sofort unnütz und die Session sollte sowohl aus Gründen der Sicherheit (s. unten) als auch zur Ressourcenersparnis sofort invalidiert werden:

// invalidate the session after logout:
session.invalidate();

Loggt sich eine Nutzerin hingegen nicht aus, so gibt es keine Möglichkeit zu erkennen, ob eine Nutzerin die Anwendung noch weiter nutzen will oder inzwischen mit anderen Dingen beschäftigt ist und womöglich sogar schon den Browser geschlossen hat. Aus dem Grund wird immer mit einem sogenannten Timeout gearbeitet. Ein Timeout ist die maximal tolerierten Zeitspanne der Inaktivität, bevor eine Sitzung als beendet gilt. Einen Standardwert für diese Frist gibt es in der Servletspezifikation nicht. Tomcat verwendet einen Timeout von 30 Minuten, bei GlassFish hingegen beträgt der Default-Timeout 10 Minuten. Wir zeigen etwas weiter unten, wie man die Standardwerte dieser beiden Server ändern kann.
Welcher Wert hier ideal ist, hängt stark von der Anwendung ab. Je sicherheitskritischer eine Anwendung ist, um so schneller sollte sie in einen Timeout laufen, wenn keine weiteren Aktionen von der Nutzerin angestoßen werden. Allerdings sind zu niedrige Werte ungünstig, wenn die Nutzerin bspw. erst noch Eingabewerte in anderen Anwendungen oder Unterlagen nachschlagen muss oder eine Anwendung im Kundengespräch genutzt wird. In aller Regel ist hier eine Abwägung zwischen der Nutzungsfreundlichkeit, die für einen möglichst großen Timeoutwert spricht, und dem damit verbundenen Ressourcenverbrauch und Sicherheitsrisiko zu treffen. Wichtig ist, dass das Timeout-Intervall nicht die maximal mögliche Gesamtzeit einer Session bestimmt. Da das Intervall mit jeder Anfrage zurück gesetzt wird, bezeichnet es lediglich die größte Zeitspanne zwischen zwei aufeinanderfolgenden Requests, bei der noch beide Requests als zur gleichen Session zugehörig betrachtet werden.
Gerade weil ein vernünftiger Timeout-Wert sehr stark von der Anwendung abhängt, ist es wichtig, diesen Wert flexibel handhabbar zu machen. Die Servlet-Spezifikation sieht entsprechend eine Konfiguration für die gesamte Anwendung in der zentralen Konfigurationsdatei "web.xml" und eine Möglichkeit zur programmatischen Anpassungs eines Timeouts einer bereits erzeugten Session vor. In den beiden folgenden Abschnitten zeigen wir diese Möglichkeiten auf.

Timeout für die Anwendung im DeploymentDeskriptor setzen

Zum Setzen eines anwendungsweiten Session-Timeouts im Deployment-Deskriptor dient das Element <session-timeout>. Dieses Element enthält als Wert den gewünschten Timeout in Minuten. Ein Wert von 0 oder ein negativer Wert bedeuten eine Session, die niemals abläuft. Aus den in den vorherigen Abschnitten beschriebenen Gründen sollte man von nicht ablaufenden Sessions allerdings absehen.
Der folgende Eintrag im Deployment-Deskriptor legt bspw. den Timeout auf 15 Minuten fest:
<web-app>
   <!-- ... -->
   <session-config>
      <session-timeout>15</session-timeout>
   </session-config>
   <!-- ... -->
</web-app>

Timeout programmatisch setzen

Denkbar sind allerdings auch Szenarien, bei denen man unterschiedliche Timeouts für unterschiedliche Nutzerinnengruppen verwenden möchte. Dies kann man mit einem Eintrag im Deployment Deskriptor nicht erreichen, vielmehr muss man hierfür programmatisch eingreifen.
Die Klasse javax.servlet.http.HttpSession sieht dazu die Methode setMaxInactiveInterval(int) vor. Dieser Methode gibt man einen Timeout in Sekunden mit.
Das folgende Beispiel würde bspw. einen Timeout von zwanzig Minuten für reguläre Accounts und fünf Minuten für einen Demo-Account zulassen. Da Demo-Accounts häufig nur kurz zum Betrachten einer Anwendung mit schneller Abfolge der aufgerufenen Aktionen genutzt werden, ist bei stark frequentierten Demo-Sites ein kurzer Timeout zugleich ausreichend und ressourcenschonend.
if (User.DEMO.equals(currUser)) {
   session.setMaxInactiveInterval(300);
}
else {
   session.setMaxInactiveInterval(1200);
}

logger.info("Interval for User " + currUser + ": " + session.getMaxInactiveInterval());

Wie man an der Log-Ausgabe im Beispiel sieht, gibt es in der HttpSession-Klasse auch die Methode getMaxInactiveInterval(), mit der der gesetzte Timeout-Wert ausgelesen werden kann.
Zu beachten ist, dass bei diesen Methoden der Timeout in Sekunden angegeben wird - im Unterschied zu einer Einstellung im Deployment Deskriptor, bei dem Minuten-Angaben verwendet werden. Ein weiterer Unterschied zur Angabe im Deployment Deskriptor ist, dass für Sessions, die niemals in einen Timeout laufen sollen, nur negative Werte und nicht auch der Wert "0" verwendet werden können.

Den Default-Timeout in Tomcat ändern

Tomcat nutzt eine besondere "web.xml"-Datei zur Definition seiner Default-Werte. Die Datei beruht auf SUNs DTD für die Konfiguratonsdatei von Servlet-basierten Webanwendungen und hier vorgenommene Einstellungen, gelten für alle Anwendungen, die nicht über eigene Definitionen verfügen. Die Datei liegt im "conf"-Verzeichnis unterhalb des Wurzelverzeichnisses der Tomcat-Installation.
Aufgrund der durch die DTD vorgegeben Struktur sieht die Definition für den Default-Timeout in dieser Datei genau so aus, wie oben für die Deployment-Deskriptor-Einstellung gezeigt:
<web-app>
   <!-- ... -->
   <session-config>
      <session-timeout>30</session-timeout>
   </session-config>
   <!-- ... -->
</web-app>

Den Default-Timeout in GlassFish ändern

Im Applikationsserver GlassFish werden alle Standardkonfigurationen in der Datei domain.xml vorgenommen. Die gilt auch für den Session-Timeout. Eine solche Datei wird für jede GlassFish-Domain7 angelegt und ist recht umfangreich. Am Besten sucht man nach dem folgenden Abschnitt und fügt bei Bedarf lediglich dass Attribut "timeout-in-seconds" dem Element "<session-properties />" hinzu, wie im Beispiel gezeigt:
<web-container>
   <session-config>
      <session-manager>
         <manager-properties/>
         <store-properties/>
      </session-manager>
      <session-properties timeout-in-seconds="600" />
   </session-config>
</web-container>

Zum SeitenanfangZum Seitenanfang

Listener

Häufig muss man wissen, wann bestimmte Ereignisse auftreten. Bspw. will man gehaltene Ressourcen frei geben, wenn eine Session beendet wird oder muss nicht-serialisierbare Ressourcen neu aufbauen, nachdem eine zuvor passivierte Session wieder aktiviert wird. Dafür kann man geeignete Listener implementieren, die entweder bei Erzeugung und Zerstörung einer Session aufgerufen werden (javax.servlet.http.HttpSessionListener), die auf das Hinzufügen und Entfernen von Objekten zu einer Session reagieren (javax.servlet.http.HttpSessionAttributeListener bzw. javax.servlet.http.HttpSessionBindingListener) oder die auf die Passivierung und Aktivierung von Session-Attributen bei verteilten Anwendung reagieren (javax.servlet.http.HttpSessionActivationListener). Listener sind allerdings Gegenstand eines eigenen Kapitels und werden daher hier nicht behandelt.

Zum SeitenanfangZum Seitenanfang

Sicherheitsprobleme im Zusammenhang mit Sessions

Sessions bieten einen beliebten Angriffspunkt, da in ihnen meist wertvolle Daten enthalten sind oder man durch eine eroberte Session erweiterte Rechte erhält. Ziel der Angriffe ist dabei immer, die eindeutige Session-ID einer Nutzerin zu erlangen. Hat eine Angreiferin diese erstmal bekommen, kann sie die Webanwendung nutzen, als wäre sie regulär angemeldet. Je nach Anwendung kann sie damit an vertrauliche Daten gelangen, kostenlos Dienste in Anspruch nehmen, die eigentlich gebührenpflichtig sind, Inhalte manipulieren, usw.
Zur Erlangung einer gültigen Session-ID gibt es mehrere Möglichkeiten, die man grob in zwei Klassen unterteilen kann. In der ersten Klasse sind alle die Angriffsvektoren enthalten, die Lücken des Applikationsservers ausnutzen. Die zweite Klasse enthält die Angriffsmöglichkeiten, die Lücken in den auf diesen Servern liegenden Anwendungen ausnutzen - also Lücken in unseren eigenen Anwendungen oder in den Frameworks, die die Grundlage unserer Anwendungen bilden.

Angriffe gegen Lücken im Applikationsserver

Alle Angriffe gegen Lücken im Applikationsserver beruhen darauf, dass die Session-ID, die vom Applikationsserver automatisch vergeben wird, nicht hinreichend sicher ist.

Gegen beide Angriffsmethoden helfen etablierte Frameworks zur Generierung sicherer, nicht-vorhersagbarer Zufallszahlen.8 Von selbstgeschriebenen Session-ID-Generatoren sollte abgesehen werden.
M.W. sind die gängigen Java-Applikationsserver und Servlet-Container schon länger nicht mehr mit Sicherheitslücken in den aufgeführten Bereichen aufgefallen, dennoch empfiehlt es sich immer, einschlägige Sicherheits-Mailinglisten zu verfolgen.

Angriffe gegen Lücken auf der Anwendungsebene

Neben den genannten Attacken, die Lücken in den Servlet-Containern und Applikationsservern ausnutzen, gibt es noch die im Folgenden aufgeführten Angriffe, die Schwächen unserer Anwendungen ausnutzen.

Unabhängig von den oben genannten Sicherheitslücken gilt, dass Cookies es der Angreiferin aufgrund der Ursprungsserver-Regel schwerer machen, eine Session-ID zu erlangen und ausnutzen zu können als die Nutzung von URL-Rewriting. Doch gibt es hinreichend Möglichkeiten, auch bei Cookies eine eroberte Session-ID ausnutzen zu können. Dazu lohnt es sich, das (englische) Whitepaper "Session Fixation Vulnerability in Web-based Applications" zu lesen, das Hintergründe nicht nur zu Session-Fixation-Attacken liefert.
Einige Empfehlungen, was man als Entwicklerin beachten sollte, gibt das Bundesamt für Sicherheit in der Informationstechnik (BSI) in seiner Broschüre "Sicherheit von Webanwendungen". Darüber hinaus lohnt auch ein Blick auf die Seite des Open Web Application Security Projects (OWASP).
Die mehrfach erwähnten Cross-Site-Scripting-Angriffe selber werden in diesem Kapitel nicht erklärt. Dazu sei auf Wikipedia und den Artikel "Cross-Site Scripting" des Sicherheits-Unternehmens Art of Defence verwiesen.

Zum SeitenanfangZum Seitenanfang

Anmerkungen:

1) Ein ähnliches Verfahren nutzt Amazon, das die Session-ID per "/" abtrennt. Dort wirkt die ID also wie ein Unterverzeichnis. (zurück)

2) Ausführliche Informationen zum Aufbau von Cookies und deren explizite Nutzung außerhalb des Session-Managements finden sich im Kapitel "Cookies setzen und auslesen". (zurück)

3) Es wäre zu weitführend, hier alle Möglichkeiten zu dokumentieren. Wer dazu Genaueres wissen will, kann dies direkt auf der englisch-sprachigen Dokumentation des Tomcat 6.0 nachlesen. Für andere Versionen muss man den Pfad entsprechend anpassen oder über die Startseite tomcat.apache.org gehen. (zurück)

4) Damit wird natürlich immer beim ersten Request das URL-Rewriting angewendet, da zu diesem Zeitpunkt der Browser noch über gar kein Session-Cookie verfügt. (zurück)

5) Interessanterweise ist vielfach die Dauer der aktiven Nutzung einer Webanwendung kleiner als die Zeitspanne, die danach auf den Session-Timeout gewartet wird. In dem Fall werden mehr Ressourcen durch inaktive denn durch aktive Sessions gehalten! (zurück)

6) In Einzelfällen kann es angemessen sein, bewusst von dieser Empfehlung abzuweichen. Das ist aber eine Abwägung, die sorgfältig getroffen werden muss. In aller Regel ist in Projekten eher ein zu sorgloser Umgang mit der Session zu beobachten. (zurück)

7) Eine Domain in GlassFish hat nichts mit einer Domain im Sinne des "Domain Name System" zu tun, sondern kennzeichnet lediglich einen administrativen Bereich. Selbstverständlich können in einer GlassFish-Domain mehrere virtuelle Domains verwaltet werden. (zurück)

8) Zahlreiche Zufallsgeneratoren basieren auf Algorithmen, bei denen sich aus einer Folge von Zufallszahlen zukünftige Zufallszahlen vorhersagen lassen. Dem stehen kryptographisch sichere Zufallszahlen gegenüber, bei denen das Vorhersagen zukünftiger Zahlen anhand einer Folge bekannter Zahlen mathematisch nur dem Prinzip nach möglich, aufgrund seiner Komplexität jedoch praktisch unmöglich ist. (zurück)

9) Je nach Zielsystem muss die Angreiferin durch wiederholtes Aufrufen bestimmter Seiten dafür sorgen, dass die Session nicht in einen Timeout läuft. Ein kurzer Timeout ist also keine Sicherung gegen diesen Angriff. (zurück)

10) Cross-Site-Scripting wird häufig mit XSS abgekürzt, seltener mit CSS. (zurück)

11) Durch das Einbetten des Links in das "src"-Attribut eines angeblichen Null-Pixel-Bildes kann die Aktion auch angestoßen werden, ohne dass die Nutzerin überhaupt irgend eine Möglichkeit hat, dies mitzubekommen. (zurück)


www.jsptutorial.org
© 2005, 2006, 2007