PHP/Einführung in die Sitzungsverwaltung

Aus Mikiwiki
< PHP
Version vom 26. Februar 2010, 23:30 Uhr von Michi (Diskussion | Beiträge)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Wechseln zu: Navigation, Suche

Sitzungsverwaltung

Zur Kontrolle der Sitzungen (engl. sessions) ist die sogenannte Sitzungs- oder Sessionverwaltung notwendig. Beim Umgang mit den ersten umfangreicheren Skripten wird schnell klar, welche schwerwiegenden Nachteile HTTP hat. Der verbindungslose Ablauf ist zwar bei kleinen Skripten vorteilhaft, weil die Datenübertragung flexibel, störsicher und einfach abläuft. Aus mehreren Formularen zusammengesetzte Skripte bereiten jedoch Schweierigkeiten. Nutzer können, wenn sie auf der folgenden Seite landen, nicht ohne weiteres wieder identifiziert werden. Der HTTP-Server vergisst sämtliche Informationen, die er aus der letzten Transaktion ermittelt hat. Ob eine Nutzer eine Seite zweimal betritt oder ob es sich um zwei verschiedene Nutzer handelt, kann nicht sicher unterschieden werden.

Zur Lösung dieser Problematik gibt es verschiedene Lösungen. Bestimmte Eigenschaften des Apache können genutzt werden, andere jedoch nur, wenn PHP als Modul einkompiliert wurde. Steht kein Apache zur Verfügung, so schränkt das die Möglichkeiten etwas ein. Bei der Auswahl der Methode zur Sitzungsverfolgung gibt es grundsätzlich die folgenden Wege.

  • Methoden mit clientseitiger Unterstützung
    • Speichern des Status in versteckten Feldern (engl. hidden fields)
    • Nutzung von Cookies
    • Speichern des Status im URI
  • Methoden mit serverseitiger Unterstützung
    • Nutzung des Prozessspeichers des Apache HTTP-Servers
    • Speichern in einer Datei
    • Speichern in einer Datenbank

Jede Methode hat besondere Vor- und Nachteile. Die clientseitigen Techniken verursachen kaum einen höheren Overhead bei der Datenübertragung und beanspruchen den Webserver nicht. Nachteilig ist die erforderliche Zusammenarbeit der Webbrowser, die nicht immer sichergestellt werden kann. Abgesehen davon bieten sich mit der Auslagerung von Funktionen in den Webbrowser auch umfangreiche unerwünschte Manipulationsmöglichkeiten - viele erfolgreiche Site-Hacks basieren auf der Ausnutzung dieser Methoden. Vor allem versteckte Felder und URI-Daten sind ohne weiteres im Quelltext sichtbar. Etwa wird bei kommerziellen Einkaufsseiten sehr oft versucht, die sichtbaren Daten zu manipulieren, um Funktionen zu stören oder ungerechtfertigten Zugriff zu erlangen. Cookies sind nicht ganz so einfach zu manipulieren, werden aber von vielen Nutzern nicht akzeptiert und schränken die Einsatzbreite einer Site etwas ein.

Serverseitige Techniken sind unabhängig vom Webbrowser und weniger anfällig für Angriffe. Speichert der Webserver die Sitzungsdaten im Speicher, sind erhebliche Zugriffe auf die Systemressourcen zu beobachten. Auch wenn nur wenige Byte gespeichert werden, so wird oft weitaus mehr Speicher benötigt. Serverseitige Operationen sind komplexer und schwerer zu implementieren. So muss der Umgang mit abgerissenen Verbindungen beachtet, die "Haltezeit" für eine Sitzung berechnet und gleichzeitig der Speicherverbrauch überwacht werden. Die Bedienung kann unter Umständen erschwert werden, denn der Webbrowser selbst hat kein eindeutiges Merkmal. Alle übertragenen Daten wie Browsername, Betriebssystem oder Version sind nicht eindeutig genug. Auch die IP-Nummer ist nicht geeignet, denn einige Internetdienstanbieter vergeben die IP-Numemr nicht nur beim Verbindungsaufbau dynamisch, sondern wechseln sie auch während der Sitzung. Ohne die Unterstützung mit einer der clientseitigen Techniken bleibt nur die Hilfe des Nutzers - Benutzername und Passwort einzugeben.

Bei der Auswahl der Technik ist auch das Ziel zu beachten. Wird nur eine Verfolgung über einige Skripte benötigt, so sind versteckte Felder und URIs zu bevorzugen. Soll ein Nutzer auch am nächsten tag wiedererkannt werden, so sind Cookies eine Variante. Wechseln die Nutzer häufig die Rechner, so kann nur eine datenbankbasierte Lösung helfen.

Sitzungen mit versteckten Feldern

Neben der bekannten Sitzungs-ID, die den Nutzer eindeutig identifiziert, ist oft auch die Übertragung verschiedenster Einstellungen nötig. Die Weiterreichung von Skript zu Skript ist eine sehr ressourcensparende Methode, denn sonst müssten die aktuellen Einstellungen für alle Nutzer auf dem Server gespeichert werden.

Versteckte Felder können nur genutzt werden, wenn die Bewegung des Nutzers von Skript zu Skript über Formulare erfolgt. Der einzige Grund, keine Formulare zu verwenden, liegt oft in den unerwünschten Schaltflächen - diese müssen jedoch nicht zwingend verwendet werden. Das folgende Tag ersetzt den Absendeschalter.

<input type="image" src="bilddatei.gif">

Das folgende Beispiel zeigt (noch ohne PHP-Sitzungsfunktionen) wie eine Sitzungs-ID im Bedarfsfall erzeugt und in einem versteckten Feld gespeichert wird. Dabei muss der Name des Tags "name=session" dem der Variablen entsprechen. Auf der nächsten Seite wird dieselbe Sequenz erneut ausgeführt. Wurde ein verstecktes Feld übergeben, so erzeugt PHP automatisch eine Variable mit dem Namen und dem Inhalt des Feldes. Die Übergabe auf das nächste Skript erfolgt mit dem "<form>"-Tag - versteckte Felder ausserhalb dieses Tags bleiben wirkungslos.

<?php
if (!isset($_POST['session'])) 
{
  $seed = "SeedwertFuerSchluessel" . $_SERVER['REMOTE_ADDR'] . time();
  $session = md5($seed);
} 
else
{
  $session = $_POST['session'];
}
echo <<<FORM
  <form action="{$_SERVER['PHP_SELF']}" method="post">
    <input type=hidden value="$session" name="session" />
    <p>Session ID : $session</p>
    <input type=submit value="Weiter... " />
  </form>
  <a href="{$_SERVER['PHP_SELF']}">Neustart</a>
FORM;
?>

Session ID : 87859093d011d22a75075630deba39d0

[Weiter...] Neustart

Es ist sinnvoll, die auf jeder Seite erforderliche Abfrage der Informationen in einer Datei abzulegen, die mit "include" eingebunden wird. Stehen die Informationen auf einer der folgenden Seiten nicht zur Verfügung, so sollte eine Umleitung auf die Startseite erfolgen.

Spielen sich alle Funktionen in ein und demselben Skript ab, so kann die Variable "$_SERVER['PHP_SELF']" genutzt werden, um das Ziel des Formulars anzugeben. Die Schreibweise in HTML sieht wie folgt aus.

<form action="{$_SERVER['PHP_SELF']}" method="post">

Innerhalb eines PHP-Skripts wird die Zeile wie folgt geschrieben.

echo "<form action=\"{$_SERVER['PHP_SELF']}\" method=\"post\">";

Im obigen Beispiel werden dagegen die bequeme "heredoc"-Syntax und ausserdem folgende Servervariable verwendet.

<form action="{$_SERVER['REMOTE_ADDR']}" method="post">

Sitzungen mit URI

Statt der Übertragung mittels HTTP-POST wie bei Sitzungen mit versteckten Feldern, kann auch der URL selbst erweitert und damit die "GET"-Methode genutzt werden. Hier kann also einfach jeder weiterführende Link um entsprechende Informationen ergänzt werden.

Wie schon bei den versteckten Feldern liegen die so übertragenen Informationen recht offen da. Mit Verschlüsselungstechniken und aufwendigen Zufallsgeneratoren kann zwar ein gewisser Schutz erreicht werden, Manipulationen sind und bleiben aber immer ein Risiko, und sei es nur, dass sich unkundige Nutzer am Code zu schaffen machen und unwissentlich Schaden anrichten. Damit erst gar niemand auf die Idee kommt, nach Informationen im URI zu suchen, kann alles in einem unsichtbaren Frameset versteckt werden. Mit dem folgenden Tag wird ein Frame definiert, das 100% des Browserfensters für sich in Anspruch nimmt, das andere Frame darf dagegen den "Rest" nutzen. Laufen die Skripte nun innerhalb des sichtbaren Frames (das unsichtbare wird nicht genutzt), so werden die Daten im Browser nicht sichtbar. Sie sind zwar weiter im HTML-Quelltext zu sehen, aber man wird nicht so offensichtlich darauf gestossen.

Sitzungen mit Cookies

HTTP ist ein verbindungsloses Protokoll. Jeder Anforderung steht eine Antwort gegenüber, Response folgt auf Request. Dazwischen geht die Verbindung praktisch verloren. Der Webserver kann nicht erkennen, ob die nächste Anforderung für denselben Webbrowser oder einen anderen bestimmt ist. Da IP-Nummern oft dynamisch vergeben werden und bei einigen Internetdienstanbieter sogar während des Surfens wechseln, eignen sich die Verbindungsdaten nicht zur Zuordnung. Solche Abläufe, bei denen sich der Nutzer auf einer Site bewegt, werden als Sitzungen (engl. sessions) bezeichnet. Eine der wichtigsten Aufgaben der Cookies ist es, eine Sitzung zu speichern und dem Webserver damit die Verfolgung des Nutzers zu ermöglichen.

Cookies bieten die einfachste Methode, um Sitzungen zu verwalten. Dabei wird am Sitzungsbeginn ein Cookie gespeichert, das kein Verfallsdatum enthält. Der gespeicherte Inhalt muss den Nutzer eindeutig identifizieren. Eine Kombination aus IP-Nummer und Zufallszahl hat sich dabei bewährt. Zum Schutz der angezeigten Daten vor Manipulationen kann ausserdem eine MD5-Verschlüsselung erfolgen. Eine "Rücksetzung" ist dagegen nicht notwendig - es geht ja nur um die Feststellung "wer ist wer?".

Auf der Startseite der Anwendung wird die aktuelle Sitzungs-ID abgefragt. Ist keine vorhanden, so wird das entsprechende Cookie erzeugt und gespeichert. Alle folgenden Seiten fragen dies ebenfalls ab und identifizieren damit den Nutzer eindeutig. Folgendes Beispiel erzeugt die Sitzungs-ID aus der um die Punkte erleichterten IP-Adresse ("61.207.3.6" wird so zu "6120736"), erhöht um einen Zufallswert zwischen 100'000 und 999'999. Diese Zahl wird mit dem MD5-Algorithmus kodiert, woraus eine eindeutige Hexadezimalzahlen-Kombination entsteht. Alternativ zu "md5" kann der Hashwert auch mit "sha1" berechnet werden.

<?php
if (!isset($_COOKIE['sessionid']))
{
  mt_srand((double) microtime()*1000000);
  $sessionid = md5(str_replace(".", "", $_SERVER['REMOTE_ADDR'])
               + mt_rand(100000,999999));
  setcookie("sessionid", $sessionid);
} 
else 
{
  $sessionid = $_COOKIE['sessionid'];
}
?>
<h3>Session-ID in Cookie speichern</h3>
<?php
echo "Es wurde folgende ID erzeugt: $sessionid";
?>

Session-ID in Cookie speichern

Es wurde folgende ID erzeugt: 1645ee8c83932d47ce4b0fa5a33fb7cb

Wenn die IP-Nummer nicht einbezogen werden soll, kann auf die Funktion "uniqid" zurückgegriffen werden, welche die aktuelle Zeit in Mikrosekunden als Basis nimmt. Als Parameter kann ein Präfix angegeben werden, der den Zeitstempel verschiebt. Damit wird verhindert, dass auf mehreren parallel laufenden Rechnern identische IDs erzeugt werden. Unabhängig von dieser Vorgehensweise ist auch hier die Verschlüsselung mit MD5 anzuraten.

$sessionid = md5(uniqid(rand()));

Textdateien und Datenbanken

Normalerweise gehören zu einer Sitzung viele Daten, nicht nur die blanke Sitzungs-ID. Diese zusätzlichen Daten können zwar mit "GET", "POST" oder über Cookies weitergereicht werden, das ist aber nicht immer ratsam.

Beispielsweise könnte die Sitzungs-ID per URI oder Formular weitergegeben, die zusätzlich nötigen Daten aber in einer gleichermassen eindeutig benannten Datei gespeichert werden. Dieses Verfahren stellt allerdings für den Webserver eine verhältnismässig grosse Belastung dar.

Die Datenbanktechnik wird in späteren Kapiteln behandelt. Beispielsweise könnten die Sitzungsdaten in einer MySQL-Datenbank abgelegt werden und wären so vor Manipulationen sicher.

In allen Fällen ändert diese Vorgehensweise nichts am Grundsatz des Umgangs mit Sitzungs-IDs. Die Skriptsteuerung wird nur für den Benutzer weniger transparent und damit schlechter angreifbar. Für grosse Projekte kann ausserdem deutlich universeller und damit wartungsfreundlicher programmiert werden.

Sicherung der Client-Techniken

Der Sinn einer MD5-Verschlüsselung der Daten besteht vor allem darin, eine Manipulation sinnlos erscheinen zu lassen und die auf dem Rechner des Nutzers gespeicherten Daten nicht zu einer wertvollen Angriffswaffe in den Händen eines Crackers werden zu lassen.

Beispielsweise werden die Kreditkartennummern der Kunden gespeichert, damit diese ihre Daten nur einmalig über das Netz senden müssen. Um Nutzer während der Sitzung wiederzuerkennen, werden versteckte Felder genutzt. Verlässt ein Nutzer nun seinen Arbeitsplatz und schaltet den Rechner aus, so kann der Webserver dies nicht erkennen. Er wird die Sitzung erst einige Zeit später "vergessen". Ein anderer Nutzer geht nun an den Rechner, durchsucht das Verzeichnis mit den gespeicherten Seiten (Cache) und ruft die zuletzt genutzte Seite auf. Er wird damit als Nutzer wiedererkannt und kann die letzte Sitzung unmittelbar fortsetzen. Vielleicht kann er dann auf die Kreditkartendaten zugreifen, die auf einer Serviceseite geändert werden können.

Zur Sicherung müssen Sitzungen mit einem engen Zeitraster belegt werden. Nach einer bestimmten inaktiven Zeit sollten Sitzungen ungültig werden und der Nutzer muss sich neu anmelden. Diese Technik nutzen auch Banken - so wird etwa nach zehn Minuten Inaktivität die erneute Eingabe eines Passworts angefordert.

Es macht also Sinn, innerhalb der Übertragung der Nutzerdaten auch die ursprüngliche Zeit zu verstecken. Und um nicht so offen dieses Verfahren zu propagieren, wird ein weiterer MD5-Wert erzeugt. Dieser Wert sollte nicht als eindeutige Sitzungsnummer hergenommen werden, denn MD5 ist kein Zufallsgenerator; gleiche Zeiten erzeugen auch gleiche Werte, sodass viele Nutzer denselben Zeitstempel haben können.

<input type="hidden" value="<?php echo md5(time); ?>" name="trst"

Sitzungen zeitlich begrenzen

Aus Sicherheitsgründen kann es erforderlich sein, Sitzungen nach einer bestimmten Zeit der Inaktivität zu unterbrechen und die erneute Eingabe der Anmeldedaten zu verlangen. Das folgende Skript zeigt eine solche Vorgehensweise. Zur Berechnung der Dauer einer Sitzung wird (zusätzlich zur hier nicht ausdrücklich ausgeführten Sitzungs-ID) ein Zeitstempel mitgeführt.

<?php
## Dauer der maximalen Inaktivität in Sekunden
$timeout = 5;
## Erkennung des Anfangszustands beim ersten Aufruf des Skripts
if (!isset($_GET['first'])) 
{
  $first = 1;
}
else 
{
  $first = 0;
}
## Erkennung der aktuellen Zeit bei jedem Durchlauf
$nextid = time();
## Die Zeit wird so umgerechnet, dass sich die resultierenden Werte erst
## ändern, wenn die Zeit überschritten wurde. Alter und neuer Wert werden
## verglichen. Der Vergleich wird beim ersten Skriptdurchlauf unterdrückt.
$id = isset($_GET['id']) ? $_GET['id'] : time();
if ((($nextid - $id) > $timeout) & !$first) 
{
  $diff = $nextid - $id;
  echo <<<OUT
    Timeout<br>
    Die Session war mehr als <b>$diff</b> Sekunden inaktiv.
    <a href="{$_SERVER['PHP_SELF']}">Neustart der Session</a>
OUT;
  ## Bei Zeitüberschreitung wird das Skript beendet
  exit;
} 
else 
{
  echo "<p>Session läuft!</p>";
  ## Beim ersten Durchlauf oder innerhalb der aktiven Zeit wird der
  ## aktuelle Zeitstempel weitergesetzt.
  $first = 0;
  $id = $nextid;
}
## Das Skript ruft sich selbst auf und gibt die Daten weiter
echo <<<FOOTER
  <p><a href="{$_SERVER['PHP_SELF']}?id=$id&amp;first=$first">Restart</a></p>
FOOTER;
?>

Session läuft!

Restart