Server Side Cookies

Heute möchte ich mir mal Cookies vornehmen. Relativ viele Webseiten benutzen diese Datenschnipsel, um alle möglichen Daten über den User zu speichern. Das Problem von Cookies ist, dass alle Cookies, die zu der Domain der aktuell geladenen Seite passen, bei jeder Ressource erstmal vom Client im HTTP-Request geladen werden müssen. So kommt recht schnell eine ordentliche Datenmenge zusammen. In diesem Beitrag möchte ich eine Idee vorstellen, wie man diese Cookiegröße konstant und auch noch konstant klein halten kann.

Anstoß für diesen Beitrag war unter anderem Der Einfluss von Cookies auf die Performance einer Webseite von David Müller. Er beschreibt darin ein übliches Szenario moderner Webseiten:

MeineWebseite.com verpasst euch beim ersten Besuch 5 Cookies, jeder 600 Byte groß. Jetzt besteht die Seite aus 30 Images, 4 Javascripts und 8 CSS-Files – garkein unübliches Setup. Das Dokument selbst noch hinzugerechnet haben wir also 1+30+5+8 = 44 HTTP-Requests.

Ich komme zwar nur auf 43 HTTP-Requests (1+30+4+8), aber das spielt hier auch keine Rolle.

David beschreibt dann die Empfehlung Use Cookie-free Domains for Components, den das Firebug-Plugin YSlow empfiehlt. Hierbei werden alle statischen Komponenten wie Bilder, CSS-Dateien und JS-Dateien in einen anderen Ordner gelegt, auf den man dann mittels einer Subdomain zugreift (z.B. static.example.org). Die Cookies dürfen dazu natürlich nur für die Domain “www.example.org” gesetzt werden, über die alle dynamischen Webseiten erreichbar sind.

Das ist auch genau so zu empfehlen. Punkt.

Ich habe mir aber in letzter Zeit mal Gedanken gemacht, wie man die Cookie-Größe der Anfragen auf nicht-statische Seiten verkleinern kann und dabei habe ich mir ein System ähnlich den Sessions ausgedacht und das ganze Server Side Cookies genannt. Falls dieses Konzept unter einem anderen Namen bekannt ist, bitte ich um einen kurzen Kommentar.

Das Konzept basiert wie Sessions auf einer ID, die für jeden Besucher eindeutig ist. Diese Cookie-ID wird als ganz normaler Cookie beim User gespeichert. Alle Nutzdaten aber werden serverseitig gespeichert.
Über die Cookie-ID kann dann auf die einzelnen Werte zugegriffen werden.
Diese Methode hat verschiedene Vorteile:

  • konstante Größe der HTTP-Header, egal, wie viele und wie große Daten man in Cookies speichern möchte -> bessere Performance & Skalierung
  • keine Übertragung für die aktuelle Anfrage nutzloser Informationen
  • keine Anzahl- und Größenbeschränkung von Cookies, sonst nur 300 Cookies à max. 4 kB pro Domain erlaubt
  • geringe Anfälligkeit gegen Manipulationen (Validierung der Cookie-Werte muss nur beim Schreiben erfolgen, nicht beim Lesen)

Der einzige Nachteil, der mir eingefallen ist, ist, dass die gespeicherten Informationen mittels der Cookie-ID auf dem Server erstmal ermittelt werden müssen, während sie mit normalen Cookies natürlich sofort vorhanden sind. Inwiefern aber die notwendige Validierung normaler Cookies, die bei den Server Side Cookies nicht nötig ist, diesen zusätzlichen Aufwand bereits auffrisst, kann ich nicht 100%ig sagen.
Weitere Nachteile sind mir erstmal nicht eingefallen, bestimmt gibts aber welche, also bitte ich um einen Kommentar, falls euch weitere einfallen.

Für mein Experiment habe ich folgende Datenbank-Tabelle benutzt:

CREATE TABLE IF NOT EXISTS `cookies` (
  `cookie_ID` CHAR(23) NOT NULL,
  `name` VARCHAR(255) NOT NULL,
  `value` text NOT NULL,
  `expires` INT(10) UNSIGNED NOT NULL,
  PRIMARY KEY (`cookie_ID`,`name`),
  KEY `expires` (`expires`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Zum Schreiben und Lesen der serverseitigen Cookies habe ich folgenden Code erstellt:

function setServerCookie($name, $value, $expires=null)
{
  if (empty($_COOKIE['id'])) {
    do {
      $cookieID = uniqid('', true);
      $cookie_result = mysql_query("
        SELECT 1
        FROM cookies 
        WHERE cookie_ID='" . $cookieID . "'
        LIMIT 1"
      );
    } while (mysql_num_rows($cookie_result) > 0);
    setcookie('id', $cookieID, time() + 86400 * 30, '/', 
     'www.example.org', false, true);
  } else {
    $cookieID = $_COOKIE['id'];
  }
 
  if(is_null($expires)){
    $expires = time()+3600*3;
  }
 
  mysql_query("
    INSERT INTO cookies (
      cookie_ID, 
      name, 
      value, 
      expires
    ) VALUES (
      '" . mysql_real_escape_string($cookieID) . "', 
      '" . mysql_real_escape_string($name) . "', 
      '" . mysql_real_escape_string($value) . "', 
      '" . intval($expires) . "'
    ) ON DUPLICATE KEY UPDATE 
      value='" . mysql_real_escape_string($value) . "', 
      expires='" . intval($expires) . "'"
  );
}
 
function getServerCookie($name)
{
  if (!isset($_COOKIE['id'])) {
    return false;
  }
  $cookie_result = mysql_query("
    SELECT value 
    FROM cookies 
    WHERE 
      cookie_ID='" . $_COOKIE['id'] . "' AND 
      name='" . mysql_real_escape_string($name) . "' AND 
      expires>=" . time() ." 
    LIMIT 1"
  );
  if (mysql_num_rows($cookie_result)) {
    return mysql_result($cookie_result, 0);
  }
  return false;
}

Als Algorithmus für die Erstellung der Cookie-ID nutze ich uniqid(). Außerdem überprüfe ich, ob es nicht eventuell bereits einen anderen Cookie mit der gleichen Cookie-ID gibt. Das schützt davor, dass es im unwahrscheinlichen Falle, dass 2 Besucher die gleiche Cookie-ID erhalten, zu Komplikationen kommt, da dann einer von den beiden eine andere Cookie-ID erhält (die uniqid()-Berechnung wiederholt wird).

Das Verfahren ist natürlich auch auf Dateibasis möglich.

Als Beispiel, was das ganze bringt, nehme ich nochmal die Zahlen von oben: 5 Cookies, jeder 600 Byte groß.
Die Requests für statische Ressourcen sind bereits durch die oben erwähnte Subdomain-Verschiebung eliminiert.
Für den Rest:

  Normale Cookies Server Side Cookies
Größe im HTTP-Header 3000 B 23 Byte

Was haltet ihr von dieser Variante der Speicherung von Cookies?

Dieser Beitrag wurde in   PHP veröffentlicht.
Fügen Sie ein Lesezeichen für den   permanenten Link hinzu.

Jan hat 152 Beiträge geschrieben

25 Kommentare zu “Server Side Cookies

  1. Alexander K. sagt:

    Weiterer Nachteil: Das auslesen via JS ist nicht mehr möglich.

    Weiterer Vorteil: Cookies müssen nur geladen werden, wenn sie wirklich benötigt werden. Sprich wenn z.B. der User eingeloggt ist.

  2. Jonathan sagt:

    Ganz verstehe ich den Artikel nicht. Der erste Teil mit der Cookie-freien Domain ist einleuchtend. Alles was danach kommt, macht doch eine ganz normale PHP-Session auch.

  3. Jan sagt:

    @Jonathan: Eine Session ist so ähnlich, außer dass die Session zerstört wird, wenn der Besucher den Browser schließt. Mit einem Cookie kann man Informationen über mehrere Besuche hinweg speichern.

  4. Oliver sagt:

    @Jan:
    Warum soll denn die Session zerstört werden, wenn man den Browser schliesst? Du kannst doch die Cookie Einstellungen mit session_set_cookie_params beliebig ändern. Allerdings ist das nicht ganz ungefährlich. Der Sessioncookie sollte dann zumindest verschlüsselt sein und sich regelmässig ändern. Würde man dann noch die Session direkt auf mysql auslagern, z. B. über die php.ini könnte man das einfach so benutzen.

    Eine schönere und datenschutzfreundlichere Variante für ist übrigens localStorage. Dort hat man aber das Problem, dass man die Daten irgendwie anders zum PHP schaffen muss (Get/Post/Temporäres Umschreiben zum Sessioncookie), weil der Server nicht direkt darauf zugreifen kann. 🙂

  5. GhostGambler sagt:

    Das gleiche kann man auch mit weniger Zeilen an Code erreichen.
    Die Session-Cookie-Lifetime abändern:
    http://de.php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime

    Ein Timestamp in $_SESSION, der den letzten Zugriff enthält. Wenn dieser lange her ist, werden „intime“ Daten „gesperrt“ – irgendeine 2-zeilige Funktion. Alles unwichtige ist immer abrufbar.

    Hat nebenbei den Vorteil, dass man den Session-Handler überschreiben kann, das superglobale Array nutzen kann, auch Arrays in dem “Cookie”/der Session speichern kann, und bestimmt noch mehr, was mir gerade nicht einfällt.

  6. Ich würde es immer genau so machen wie in etwa beschrieben. Mir kam bisher nie in den Sinn irgendwelche Benutzerbezogenedaten in ein Cookie zu speichern. Daher vergebe ich, seit je her nur eine verschlüsselte ID. Und mit dieser kann der Benutzer, sofern autorisiert, auf seine Daten auch über Java-Script bzw. AJAX zugreifen und diese Daten ggf. nachladen, sofern das wirklich gewünscht sein sollte.

    Wichtig ist vielleicht wirklich noch die Verschlüsselung. Nicht, das man sich nach her einloggt und autorisiert ist, und durch das manuelle anpassen des Cookies auf fremde Daten zugreifen kann.

  7. jule_ sagt:

    Solche “Server Side Cookies” kann man aber nicht auf Javascript Ebene ansprechen, zB um sich zu merken in welchem Tab einer Applikation ein Benutzer war.

  8. Marco sagt:

    Hab ich selber bisher immer so gemacht, da ich auch gerne selber die Kontrolle über die Sessions habe und daher nicht auf die session_*-Funktionen zurückgreife.
    Einfach eine SID erzeugen, diese per Cookie an den User und der Rest läuft serverseitig.
    Ich sehe den Vorteil hinterher auch bei der Validierung. Habe ich nur 1 Cookie, muss ich auch nur 1 Cookie auf Existenz prüfen.
    Bei mehreren Cookies braucht der User ja nur 1 Cookie löschen, bringt dadurch einiges durcheinander und man muss jeden einzelnen relevanten Cookie auf Existenz prüfen, was zu einer elendig langen if else Verschachtelung führt.

  9. Ralf sagt:

    session.cookie_lifetime specifies the lifetime of the cookie in seconds which is sent to the browser.

    Verstehe ich mal so, dass man die Session beenden kann bevor der Browser geschlossen wird. Nicht so, dass man die Session über die Verbindungsdauer hinweg speichern kann.

    Wahrscheinlich ist hier eher session.gc_maxlifetime gemeint. Damit gibt man an nach wie vielen Sekunden (!!!) eine gespeicherte Session als Müll angesehen wird.
    Da der Garbage Collector früher oder später aber eh zuschlägt, selbst wenn man den Wert für session.gc_maxlifetime sehr hoch ansetzt, ist die “gespeicherte” Session spätestens dann weg.

    Sessions sind also nur bedingt eine Alternative zu der oben gezeigten Lösung die die Daten dauerhaft speichert. Das wird alleine schon dadurch deutlich, dass alle Zeitangaben in Sekunden gemacht werden müssen, was gegen eine langfristige Speicherung über Tage, Wochen oder Monate hinweg spricht.

  10. GhostGambler sagt:

    Natürlich kann man dem Cookie mitteilen, dass er auch über das Beenden hinweg bestehen bleiben sollte. Die Einstellung dort, ist einfach nur das, was bei setcookie sonst übergeben wird. Das Session-Cookie unterscheidet sich praktisch nicht von einem setcookie-Cookie. Nur dass die Parameter nicht der Funktion, sondern der Konfiguration übergeben werden.

    An den Garbage-Collector hat bisher in der Tat niemand gedacht, guter Punkt. Im Endeffekt ist das aber auch nur ein Algorithmus, den man dahingehend anpassen kann, dass er nur sehr sehr alte Sessions löscht. (Das ist noch mal ein 5-Zeiler oder so.)
    Ansonsten kann man den auch einfach deaktivieren.

  11. Ralf sagt:

    Ansonsten kann man den auch einfach deaktivieren.

    Na da wünsche ich viel Spaß beim manuellen Entmisten der alten Session-Daten.

    Wenn überhaupt, könnte man die Session-Daten kopieren und woanders speichern. So was setzt jedoch immer voraus das man auch Zugriff auf die entsprechenden Daten hat (gleiches gilt für den GC). Bei einem Shared Host dürfte das nicht der Fall sein.

    Einen Garbage Collector vermisse ich allerdings bei der obigen Lösung. Je nach Besucheranzahl kann da schon einiges an Daten auflaufen. Vor allem wenn man sehr viele Daten speichert wäre es dringend anzuraten ab und an aufzuräumen.

    Und hier würde ich auch die Grenze ziehen. Geht es um Daten die nach ein paar Tagen gelöscht werden können, kann man auf Sessions zurückgreifen. Will ich die Daten jedoch länger vorhalten, würde ich eher eine eigene Lösung bevorzugen.

    Im Grunde genommen müsste man beides auch mal miteinander vergleichen. Ggf. sind Sessions langsamer/schneller. Vielleicht kann man etwas an Performance raus holen wenn man nicht bei jeden Seitenzugriff auf die gespeicherten Daten zugreifen muss. Bei der Lösung mit Sessions würden ja alle Daten bei jedem Seitenzugriff geladen werden. Das könnte bei magerer Speicherausstattung und hohen Besucherzahlen eng werden.

  12. Oliver sagt:

    dass alle Zeitangaben in Sekunden gemacht werden müssen, was gegen eine langfristige Speicherung über Tage, Wochen oder Monate hinweg spricht.

    Naja, 604800 ist eine Woche. Das geht schon.

    Na da wünsche ich viel Spaß beim manuellen Entmisten der alten Session-Daten.

    Wenn man sie wie vorgeschlagen in eine mysql auslagert. Ist das keine große Zauberei. Wenn man die Sessiondaten an die Userid und nicht an eine Session ID binden kann, kommen da auch gar nicht so viele Daten zusammen.

    Vielleicht kann man etwas an Performance raus holen wenn man nicht bei jeden Seitenzugriff auf die gespeicherten Daten zugreifen muss.

    Naja, jedes Forum schreibt mehr Daten.

  13. Lars sagt:

    Die Tatsache, dass man an die auf diese Weise gespeicherten Daten nicht via JS rankommt, ist für mich das Hauptproblem dabei. Ansonsten, wenn man darauf verzichten kann bzw. das eh nicht nutzt, das durchaus eine gute Methode, um ein bisschen extra Perfomance rauszuholen.

  14. Ralf sagt:

    Naja, 604800 ist eine Woche. Das geht schon.

    >> Und hier würde ich auch die Grenze ziehen. Geht es um Daten die nach ein paar Tagen gelöscht werden können, kann man auf Sessions zurückgreifen.

    Wenn man die Sessiondaten an die Userid und nicht an eine Session ID binden kann

    Wenn ich eine UserID habe (ich gehe mal davon aus das man die hat wenn der User angemeldet ist), muss man sich um Cookies keinen Kopf machen. Die sind doch i.d.R. für nicht angemeldete Benutzer.

    Naja, jedes Forum schreibt mehr Daten.

    Und mit der Anzahl an Daten wird der Speicher (RAM) immer knapper. Man darf ja nicht davon ausgehen das jeder einen eigenen Server hat, sondern das man ggf. auch mal Software schreibt die mit sehr wenig Speicher auskommen muss.

    Wobei hier natürlich auch die Frage aufkommt, was man so alles in einem Cookie rein schreiben will das man da Probleme bekommt. Vielleicht sollte man sich als erstes mal den Grundsatz der Datenvermeidug zu Herzen nehmen.

  15. Oliver sagt:

    @Ralf
    Ich bin aber persönlich eh kein Fan davon, Sessiondaten dauerhaft auf den Server auszulagern. (Du so wie ich das lese auch nicht). Um Cookies zu vermeiden gibt es bessere Möglichkeiten. 🙂

  16. Merlin sagt:

    Schöne Idee, jedoch in der derzeitigen Umsetzung zu unsicher, da man nur die Session-ID von jemand anderem braucht um direkt als er identifiziert zu werden. AUch wenn diese verschlüsselt ist, kann der verschlüsselt übertragene Cookie abgefangen werden und schon hat man alles was man braucht.

    Mein Vorschlag:
    2 Cookies setzen, einer enthält die Session-ID in Klartext, der andere ist sowas wie ein Schlüssel.
    Für diesen Schlüssel wird bei der Generierung einer Session zum einen die Session-ID, eine User-spezifische Session-Variable, welche sich bei jedem Login ändert, die User-ID und evtl andere Daten in einer geheimen mathematischen Formel vermischt, dann um andere Daten wie zB $_SERVER[‘USER_AGENT’], $_SERVER[‘REMOTE_ADDR’] oder der hostname des Besuchers angereichert und dann per MD5 mit Salt anonymisiert. Dieser Schlüssel ist nahezu unmöglich für einen aussenstehenden zu berechnen, ausserdem wird der als Cookie übermittelte Schlüssel bei jeder Anfrage mit dem neu berechneten Schlüssel (über das selbe Verfahren, User-ID kriegt man aus der Datenbank da man ja die Session im Klartext hat) verglichen und durch die Bindung an IP, Hostname und User-Agent ist ein hoher Grad an Sicherheit gegeben.
    Ich speichere zusätzlich noch diese 3 Werte als MD5-Hash in die Datenbank zur Session und gleiche sie einzeln ab.

    Desweiteren sollte jede Session nach einer gewissen Zeit ablaufen, dazu wird ein expire-Timestamp zur Session gespeichert, welcher bei jedem Aufruf erneuert wird.
    Wird der Expire-Zeitpunkt überschritten wird die Session gelöscht und der Nutzer beim nächsten Aufruf darauf hingewiesen (Feststellbar, da er zwar Cookies hat, aber keine passende Session gefunden werden konnte).

    Mit Salt Gehashte Passwörter und eine Maximal-Anzahl an Login-Versuchen gehören natürlich auch zu jeder sicheren Session-Verwaltung, darauf gehe ich jetzt nicht näher ein.

    Wenn jemandem noch eine Verbesserung einfällt, ich bin immer offen für Neues 😉

  17. Frank sagt:

    Hey, das ganze Thema nennt man “Fingerprint-Tracking”. Im Endeffekt funktioniert es so, das man all mögliche Informationen des Benutzers (Browser, Sprache, Auflösung, OS usw.) zu einer fast eindeutigen ID zusammenfasst und man somit einen sServerseitigen Cookie erstellen kann, der mit einer Wahrscheinlichkeit von 1:2000000 eindeutig ist, wenn man alle Infos die man erheben kann (aber nicht die IP), nutzt.

    MfG Frank

  18. Ralf sagt:

    “mit einer Wahrscheinlichkeit von 1:2000000 eindeutig ist”
    Äußerst gewagte These. Man wird anhand von Browser, OS, Auflösung, usw. z.B. keine zwei iPhones oder iPads von einander unterscheiden können.
    Im übrigen geht es im obigen Beitrag nicht ums Tracking, sondern vielmehr um Sessions.

  19. Merlin sagt:

    ja ich wollte auch nicht das Thema tracking anstoßen, dafür nutze cih die Daten auch nicht. Ich nutze sie nur als zusätzliches Sicherheitskriterium damit Session-Hijacking erschwert wird. Deshalb wird auch alles gehasht gespeichert, damit nicht irgendwelche benutzerspezifischen Daten im Klartext in der Datenbank liegen.

  20. rmp sagt:

    Och Mädels,
    ihr seid doch Herr eurer PHP-Anwendung!

    Was wenn ihr Cookies für JS braucht?
    Dann liefert ihr nur die nötigen aus und lasst das JS wieder alle löschen außer das Cookie mit der Session-ID. (nix unnötig ausliefern, nix unnötig hochladen)

    Was wenn ihr keine Cookies für JS braucht?
    Dann nehmt eine langlebige Session ($_SESSION[‘cookies’]) und denkt an einen Login etc ($_SESSION[‘user’]). Beim Logout wird alles gelöscht außer die Cookie-Info / das -Array.

    Was wenn ihr keine langlebigen Infos braucht?
    “Recycled” die älteste ungültige Session-ID und hängt diese ans GET im Zahlensystem mit Basis 62. Selbst bei 3843 Besuchern pro Sessionlaufzeit macht das nur ein kleines “&s=ZZ” aus.

    Das alles stellt sich doch schon beim Modellieren heraus!

Eine Antwort schreiben

Ihre E-Mail-Adresse wird nicht veröffentlicht. Benötigte Felder sind markiert mit *

You may use these HTML tags and attributes: <a href=""> <blockquote cite=""> <pre lang=""> <b> <strong> <i> <em>