17.15 Kleine Kekse: die Klasse Cookies
Jeder Auftrag an den Web-Server wird unabhängig von anderen Aufträgen verwaltet. Wenn wir beispielweise eine Seite neu laden oder einen Verweis verfolgen, dann weiß der Server nicht (beziehungsweise interessiert sich nicht dafür), dass die Anfrage von uns kam. Was an diesem Verhalten deutlich wird, ist das Fehlen eines Zustands. Es fehlt also die Möglichkeit, dass ein Client vom Server identifiziert wird und einem aktuellen Zustand des bidirektionalen Kommunikationsverlaufes zugeordnet werden kann. Der Zustand bezieht sich hier auf eine Server-seitige Information, die nicht existiert. Aus diesem Grund wird HTTP auch als zustandsloses Protokoll bezeichnet. Dass dies aber nicht immer wünschenswert ist und sogar einen Nachteil darstellen kann, sehen wir an unterschiedlichen Anforderungen:
|
Ein Warenkorb für den Einkauf
In Online-Systemen wird ein Einkaufswagen gefüllt, unterschiedliche Web-Seiten informieren die Kaufwilligen über die Produkte. Wenn allerdings der Server die Seitenanfrage dem Client nicht zuordnen kann, so kann der Warenkorb nicht gefüllt werden. |
|
Individualisierung
Bei privaten Seiten muss sich ein Benutzer anmelden, damit er die Angebote nutzen kann. Es ist unpraktisch, wenn er sich bei jedem Seitenwechsel neu authentifizieren muss. Verlässt ein Kunde das System auf einer bestimmten Seite, so kann das System nach einem erneuten Anmelden den Benutzer wieder zurück auf diese Seite führen. Wurde vom Kunden in der Suchmaschine eine Ware gesucht, die nicht verfügbar war, so kann sich dies nach einer Zeit geändert haben. Das System sollte dem Benutzer dann die Information geben, dass seine Ware nun verfügbar ist. |
|
Demoskopie
Das System eignet sich auch für die Benutzerüberwachung. Besucht ein Benutzer eine Seite mehrmals, so kann der Betreiber dies erkennen und diese Information mit einem »Ist-Beliebt-Faktor« verbinden. Diese Information lässt sich natürlich kommerziell gut nutzen. |
Es ist also ein System gesucht, das es dem Server erlaubt, den Client zu identifizieren. Dazu dienen kleine Informationseinheiten, die Cookies. Der Server kann den Client veranlassen, diese Information eine bestimmte Zeit zu speichern. Betritt der Client die Seite des Anbieters, so schickt er dem Server den Cookie als Kennung. Dieser kann anhand der Cookie-Kennung die Sitzung erkennen, sofern er die Information gesichert hat. Der Name und die Technologie der Cookies wurde von Netscape geprägt, als diese noch den Browser-Markt revolutionierten. Mittlerweile kümmert sich die HTTP Working Group der Internet Engineering Task Force (IETF) um die Weiterentwicklung.
Das Wort »Cookie« wird gerne mit Keksen1 assoziiert, dies ist aber nicht beabsichtigt. Informatiker kennen den Begriff und meinen damit einfach nur kleine Informationseinheiten. Mehr Informationen rund um Cookies hat David Whalen auf seiner Seite http://www.cookiecentral.com/ gesammelt.
17.15.1 Cookies erzeugen und setzen
Cookies werden für den Benutzer durch die Klasse Cookie verwaltet. Sie bietet Methoden zur Bearbeitung der Informationen, die der Cookie speichert. Damit wir auf der Client-Seite Cookies setzen können, müssen wir zunächst ein Cookie-Objekt erzeugen. Dazu bietet die Klasse genau einen Konstruktor mit zwei Parametern an, die dem Cookie einen Namen und einen Wert geben. Der Name muss nach RFC 2109 geformt sein, das heißt vereinfacht aus Buchstaben und Ziffern. Nun muss der Cookie beim Client gesetzt werden. Dies führt die Methode addCookie() auf dem HttpServletResponse-Objekt durch:
Cookie cookie = new Cookie( "key", "value" );
response.addCookie( cookie );
Da es mehrere Einträge geben kann, darf die Methode auch mehrmals aufgerufen werden.
interface javax.servlet.http.HttpServletResponse
extends ServletResponse
|
|
public void addCookie( Cookie cookie )
Fügt der Anwort einen angefüllten Cookie-Header zu. |
17.15.2 Cookies vom Servlet einlesen
Bei jeder weiteren Kommunikation mit einem Server, werden die mit der Server-URL assoziierten Cookies-Daten automatisch mitgeschickt. Um sie zu erfragen, bemühen wir die Methode getCookies() des HttpServletRequest-Objekts. Der Rückgabewert der Methode ist ein Feld von Cookie-Objekten. Jeder Cookie bietet als Objektmethode getName() und getValue() an, um an die Schlüssel/Werte-Paare zu gelangen. Wenn die getCookies()-Methode null liefert, so war noch kein Cookie angelegt, und wir müssen darauf reagieren.
Listing 17.16 CookieDemo.jsp
<%@ page import="java.util.*" %>
<%
String myCookieName = "visisted";
Cookie cookies[] = request.getCookies();
if ( cookies == null )
out.println( "Kein Cookie gesetzt!" );
else
{
boolean visited = false;
for ( int i = 0; i < cookies.length; i++ )
{
String cookieName = cookies[i].getName();
if ( cookieName.equals(myCookieName) )
visited = true;
%>
Cookie "<%= cookieName %>" hat den Wert "<%= cookies[i].getValue() %>"
<%
}
if ( !visited )
{
Cookie visCookie = new Cookie( myCookieName, new java.util.Date().
toString() );
response.addCookie( visCookie );
out.println( "Cookie gesetzt" );
}
}
%>
Bekommt der Server eine Anforderung vom Client, so kennt der Client natürlich die Server-Adresse. Er schaut in seinem Cookie-Speicher nach, ob mit diesem Server ein Cookie assoziiert ist. Dann schickt er diesen automatisch in einem speziellen Cookie-Feld mit, so dass der Server diesen Wert auslesen kann. Cookies sind für andere Server nicht sichtbar, so dass sie keine direkte Sicherheitslücke darstellen.
17.15.3 Kleine Helfer für Cookies
Setzen wir mehrere Cookies im Programm, so liefert getCookies() lediglich ein Feld von Cookie-Objekten. Wollen wir einen Keks mit einem bestimmen Namen ansprechen, so müssen wir durch das Feld wandern und nach dem Cookie suchen. Dafür bietet sich eine vorteilhafte Hilfsmethode an, die das Feld nach dem Cookie durchsucht. Wir wollen die Methode getCookieValue() nennen.
public static String
getCookieValue( Cookie[] cookies, String name, String default )
{
for( int i = 0; i < cookies.length; i++ )
return name.equals( cookies[i].getName()) ?
cookies[i].getValue() : default;
}
Diese Methode hat noch einen weiteren Vorteil: Sie übergibt dem Aufrufer einen Standardwert, falls der Cookie nicht gesetzt wurde.
Eine andere Lösung besteht darin, die Cookies in ein Map-Objekt abzulegen. Dann erfolgt die Anfrage immer aus dem Assoziativspeicher und nicht mehr aus dem Feld. Der Vorteil liegt darin, dass wir einmal die Map erstellen und dann den Cookie über die Methode get() erfragen. Folgendes Programmstück erzeugt aus dem HttpServletRequest selbstständig ein HashMap-Objekt mit den Schlüssel/Werte-Paaren:
public Map getCookies( HttpServletRequest request )
{
Cookie cookies[] = request.getCookies();
Map m = new HashMap();
for ( int i = 0; i < cookies.length; i++ )
m.put( cookies[i].getName(), cookies[i].getValue() );
return m;
}
Jetzt ist es leicht, nach einem Cookie zu fragen:
Map m = getCookies( request );
if ( !m.isEmpty() )
String s = (String) m.get( key );
Der Test, ob Cookies überhaupt gesetzt sind, ist einfach. Dies ist ein Aufruf von m.isEmpty().
17.15.4 Cookie-Status ändern
Im Cookie werden neben einem Namen und dem damit verbundenen Wert noch weitere Informationen gespeichert. Die nachfolgende Aufzählung zeigt die Zugriffsmethoden für Cookies:
class javax.servlet.http.Cookie
implements java.lang.Cloneable
|
|
void setComment( String purpose )
String getComment()
Eine zusätzliche Beschreibung für den Cookie, der nicht von jedem Browser unterstützt wird (beispielsweise von Netscape). Bei der Abfragemethode bekommen wir null, falls dem Cookie kein Kommentar zugewiesen wurde. |
|
setDomain( String pattern )
String getDomain()
Der Gültigkeitsbereich eines Cookies. Der Domänenname beginnt mit einem Punkt (etwa .kuchenfuerulli.com) und gilt dann für alle direkten Rechner dieser DNS-Adresse, also etwa www.kuchenfuerulli.com, aber nicht a.b.kuchenfuerulli.com. |
|
void setMaxAge( int expiry )
int getMaxAge()
setMaxAge() setzt das Alter in Sekunden, in denen der Cookie existieren soll. Ist der Wert negativ, so wird der Cookie nicht gespeichert. Er wird nach der Sitzung, also beim Schließen des Browsers, entfernt. getMaxAge() liefert die Lebensdauer eines Cookies, dabei treffen die oben getätigten Aussagen auch hier zu. |
|
void setPath( String uri )
public String getPath()
Der Pfad gibt den Ort für den Client an, an dem der Cookie sichtbar ist. Die Sichtbarkeit gilt für das angegebene Verzeichnis und alle Unterverzeichnisse. Zusätzliche Informationen sind in der RFC 2109 abgelegt. |
|
void setSecure( boolean flag )
public boolean getSecure()
Mit einer sicheren Verbindung lassen sich Cookies nur über ein sicheres Protokoll wie HTTPS oder SSL übertragen. setSecure(true) sendet den Cookie daher nur, wenn ein sicheres Protokoll verwendet wird. getSecure() liefert false, wenn der Browser den Cookie durch ein beliebiges Protokoll senden kann. |
|
void setName( String name )
String getName()
Der Name des Cookies, der nach der Erzeugung nicht mehr geändert werden kann. |
|
void setValue( String newValue )
String getValue()
Jeder Cookie speichert einen Wert, der mit setValue() neu gesetzt werden kann, sofern das Cookie existiert. Bei einem binären Wert müssen wir selbstständig eine ASCII-Kodierung finden, zum Beispiel eine BASE64-Kodierung. Mit Cookies der Version 0 sind die Zeichen ' ', '(', ')', '[', ']', '=', ',', '\'', '»', '\', '?', '@', ':', ';' nicht zugelassen. Nicht gesetzte Werte können unterschiedliche Rückgaben des Browsers provozieren. |
|
int getVersion()
void setVersion( int v )
Die Version des Cookies, wie in RFC 2109 beschrieben. Version 0 hält sich an die Originalspezifikation von Netscape. Die Version 1 wird im RFC 2109 beschrieben; die Variante ist noch etwas experimentell. |
17.15.5 Langlebige Cookies
Für Cookies, die länger als eine Sitzung halten sollen, lässt sich mit setMaxAge() eine Zeit setzen, zu der sie gültig sein sollen. Eine praktische Klasse ist MaxAgeCookie, die im parametrisierten Konstruktor das Alter auf die Höchstzahl von einem Jahr setzt. Dies müssen aber nicht alle Browser so implementieren.
Listing 17.17 MaxAgeCookie.java
import javax.servlet.http.*;
public class MaxAgeCookie extends Cookie
{
public MaxAgeCookie( String name, String value )
{
super( name, value );
setMaxAge( 60*60*24*365 );
}
}
17.15.6 Ein Warenkorbsystem
Der Schritt von unserem Programm zu einem Warenkorbsystem für den Einkauf ist nicht mehr weit. Das Servlet verwaltet für jede Sitzung in internen Datenstrukturen alle bisher bestellten Waren. Meldet sich ein Kunde im System an, wird eine Kennung erzeugt und diese zum Client geschickt. Die Kommunikation über die folgenden Seiten geht über diese Kennung, an der der Server den Client erkennen kann. Damit wir für die Kennung nicht eine beliebige Zahl selbst erzeugen müssen, können wir Zufallszahlen nutzen. Diese liefert etwa UID().toString(), die uns ein RMI-Server zur Verfügung stellt. Folgende Funktion erzeugt einen gültigen URL-String aus dieser Kennung:
public String createID()
{
return URLEncoder.encode( new UID().toString() );
}
Nun müssen wir unsere Sitzungskennung nur noch mit den Waren verbinden, die im Korb sind. Sie können etwa in einer Liste verwaltet werden. Dazu können wir folgende Hilfsmethoden nutzen:
static public synchronized void saveItem( String id, String item )
{
List list = (List) map.get( id );
if ( list == null )
map.put( id, list = new ArrayList() );
list.add( item );
}
static Map map = new HashMap();
Wir verwalten in einem Assoziativspeicher alle IDs zusammen mit einer Liste der Waren. (Wir könnten dies auch anders realisieren, indem wir jedem Servlet eine eigene Liste geben, aber so teilen sich alle Servlets über static die Datenstruktur. Dies hat den Vorteil, dass beim Herunterfahren des Systems prinzipiell die Map serialisiert werden kann, was wir hier aber nicht implementiert haben.) Wird zum ersten Mal eine Ware eingelegt, so muss getestet werden, ob schon eine Liste angelegt wurde. Dann müssen wir nur noch die Ware hinzufügen. Das Abfragen gestaltet sich ebenso einfach:
static public synchronized String getItems( String id )
{
List list = (List) map.get( id );
return list != null ? list.toString() : "[]";
}
Aus der HashMap holen wir zuerst die Liste zur ID. Zu Testzwecken geben wir eine String-Repräsentation der Liste zurück, die bekanntermaßen alle Werte umfasst. Die Liste müsste eigentlich immer existieren, doch zur Sicherheit (defensiv programmieren und auf Wiederverwendung trimmen) testen wir auch diesen Fall.
Mit diesen Helfern gelingt es uns, Informationen unseres Warenkorbs zu sichern. Die Sitzungsdauer ist nur davon abhängig, wie lange der Cookie gespeichert ist. Über Cookies haben wir erreicht, dass wir für die Dauer einer Sitzung - oder auch länger, doch das ist für das Warensystem in der Regel nicht so wichtig - Informationen speichern können. Wir werden im folgenden Kapitel sehen, dass uns die Java-API eine schönere Lösung für dieses Problem liefert.
1 Das ist richtig für das Amerikanische, die Engländer können damit meist nicht viel anfangen, dort heißt ein Keks Biscuit.
|