mitmario.dev

Sitzungen

PHP Sandbox 4 Min Lesezeit 3 BeispieleLektion 3 von 6

In der letzten Lektion lag alles beim Besucher, und daraus folgte die Regel, dass dort nichts liegen darf, worauf du dich verlässt. Diese Lektion dreht das um: Die Daten bleiben bei dir, und der Besucher bekommt nur eine Nummer.

Zwei Zeilen, und der Wert bleibt

Ein Wert, der die Anfrage überlebt
<?php

// Trag im Browser einen Namen ein und schick ihn ab. Danach klick dich
// durch die Links: Der Name bleibt stehen, obwohl jede dieser Seiten
// ein eigener Lauf deines Programms ist.

session_start();

if (isset($_GET["name"]) && $_GET["name"] !== "") {
    $_SESSION["name"] = $_GET["name"];
}

$name = $_SESSION["name"] ?? null;

?>
<!doctype html>
<html lang="de">
<head>
    <meta charset="utf-8">
    <title>Sitzung</title>
</head>
<body>
    <h1><?= $name === null ? "Wir kennen uns noch nicht" : "Hallo, " . htmlspecialchars($name) ?></h1>

    <form method="get" action="/">
        <p><label>Dein Name: <input type="text" name="name"></label></p>
        <button type="submit">Merken</button>
    </form>

    <p>
        <a href="/">Start</a> ·
        <a href="/preise">Preise</a> ·
        <a href="/kontakt">Kontakt</a>
    </p>
</body>
</html>

session_start() am Anfang, danach ist $_SESSION ein ganz gewöhnliches Array. Du legst etwas hinein, und beim nächsten Aufruf ist es noch da. Mehr ist es aus deiner Sicht nicht.

Im ersten Beispiel trägst du im Browser einen Namen ein und klickst dich danach durch drei Links. Der Name steht auf jeder dieser Seiten, obwohl jede ein eigener Lauf deines Programms ist, mit eigenen Variablen, die es vorher nicht gab.

Was dabei wirklich passiert

Wo die Sitzung wirklich liegt
<?php

// Eine Sitzung ist kein Zauberwerk, sondern eine Datei. Diese Seite
// legt zwei Werte hinein und liest die Datei danach im Klartext vor.
//
// session_write_close() schreibt sie sofort, statt bis zum Ende des
// Programms zu warten. Nur deshalb steht unten schon etwas drin.

session_start();

$_SESSION["name"] = "Mia";
$_SESSION["korb"] = ["Kaffee", "Filter"];

$kennung = session_id();

// Ohne eigene Angabe legt PHP die Dateien in das Verzeichnis fuer
// Temporaeres, unter Linux also nach /tmp.
$ordner = session_save_path() !== "" ? session_save_path() : sys_get_temp_dir();
$datei = $ordner . "/sess_" . $kennung;

session_write_close();

?>
<!doctype html>
<html lang="de">
<head>
    <meta charset="utf-8">
    <title>Die Sitzungsdatei</title>
    <style>
        body { font-family: system-ui, sans-serif; }
        pre { background: #eee; padding: .6rem; overflow-x: auto; }
    </style>
</head>
<body>
    <h1>Deine Sitzung auf der Platte</h1>

    <p>Kennung: <code><?= htmlspecialchars($kennung) ?></code></p>
    <p>Datei: <code><?= htmlspecialchars($datei) ?></code></p>

    <pre><?= htmlspecialchars(is_readable($datei) ? file_get_contents($datei) : "nicht lesbar") ?></pre>

    <p>Der Browser kennt davon nur die Kennung ganz oben.</p>
</body>
</html>

session_start() tut zwei Dinge auf einmal, und beide sind einfach.

Erstens sucht es nach einer Kennung. Hat der Browser eine mitgeschickt, nimmt PHP sie. Hat er keine, würfelt PHP eine neue aus und schickt sie als Cookie zurück, unter dem Namen PHPSESSID. Das ist alles, was der Besucher je zu sehen bekommt: eine lange, zufällige Zeichenfolge. Im Reiter Netzwerk kannst du beiden Fällen zusehen: Bei der ersten Antwort steht Set-Cookie: PHPSESSID=… unter den Antwort-Kopfzeilen, bei jeder Anfrage danach cookie: PHPSESSID=… unter denen der Anfrage, und ein neues Set-Cookie kommt nicht mehr. Dazu schickt session_start() bei jeder Antwort drei Zeilen mit, die dem Browser das Zwischenspeichern der Seite verbieten (Cache-Control, Pragma, Expires mit einem Datum von 1981); eine Seite, die zu einer Sitzung gehört, soll er jedes Mal frisch holen.

Zweitens sucht PHP die Datei, die zu dieser Kennung gehört, liest sie und legt den Inhalt in $_SESSION. Am Ende deines Programms schreibt es sie zurück. Das zweite Beispiel liest diese Datei vor, im Klartext, mit deinem eigenen Namen und deinem eigenen Warenkorb darin. Dort steht sie auf der Platte deines Servers, und dorthin kommt der Besucher nicht. Du schon: Das Terminal läuft auf derselben Maschine wie der Server, und ls -la /tmp/sess_* zeigt die Datei, cat /tmp/sess_* ihren Inhalt, Zeichen für Zeichen das, was die Seite vorliest. Stehen dort mehrere Dateien, sind das die Sitzungen, die auf dieser Maschine vorher schon einmal angefangen haben.

Eine Kleinigkeit, die man wissen sollte: An sein eigenes Sitzungscookie hängt PHP von sich aus kein httponly, obwohl gerade dort die Kennung steht, um die es einem Angreifer geht. In der Prüfliste der nächsten Aufgabe kannst du es nachlesen, unter der Zeile, die nach der Kennung fragt: PHPSESSID=…; path=/, und sonst nichts. Auf einem echten Server schaltet man das ein, entweder in der php.ini oder mit session_set_cookie_params() vor dem session_start(). In Abschnitt 15 kommt das noch einmal.

Damit ist auch beantwortet, warum in einer Sitzung liegen darf, was in einem Cookie nichts zu suchen hat. Der Besucher kann seine Kennung ändern, natürlich. Nur landet er damit bei einer anderen Datei oder bei gar keiner, nicht bei einer Datei mit dem Inhalt, den er gerne hätte.

Warum die Zeile ganz oben stehen muss

session_start() schickt Kopfzeilen hinaus, allen voran das Cookie mit der Kennung. Damit gilt dieselbe Regel wie bei header() in Lektion 10.4 und bei setcookie() in 12.2: Sobald das erste Zeichen deiner Seite unterwegs ist, sind die Kopfzeilen weg. PHP meldet dann session_start(): Session cannot be started after headers have already been sent, in diesem Kurs mitten auf der Seite und im Log, wie in 10.4 beschrieben. Und der Browser bekommt keine Kennung, also fängt er beim nächsten Aufruf eine neue Sitzung an.

Die Zeile gehört deshalb nach ganz oben, vor jedes echo, vor jedes Leerzeichen und vor jede schließende Marke. Das ist übrigens der eigentliche Grund, warum eine reine PHP-Datei am Ende kein ?> bekommt, wie du es in Lektion 1.3 gelernt hast: Ein einziger Zeilenumbruch hinter der Marke wäre schon Ausgabe.

Wie eine Sitzung endet

Die Sitzung beenden
<?php

// Zaehl im Browser ein paar Mal hoch und raeum danach auf. Nach dem
// Aufraeumen faengt der Zaehler wieder bei eins an, und in der
// Adresszeile aendert sich nichts.

session_start();

if (isset($_GET["ende"])) {
    // Drei Schritte, und alle drei gehoeren dazu.
    $_SESSION = [];
    setcookie(session_name(), "", ["expires" => time() - 3600, "path" => "/"]);
    session_destroy();

    header("Location: /", true, 302);
    exit;
}

$_SESSION["stand"] = ($_SESSION["stand"] ?? 0) + 1;

?>
<!doctype html>
<html lang="de">
<head>
    <meta charset="utf-8">
    <title>Aufraeumen</title>
</head>
<body>
    <h1>Stand: <?= $_SESSION["stand"] ?></h1>

    <p>
        <a href="/">Weiterzaehlen</a> ·
        <a href="/?ende=1">Sitzung beenden</a>
    </p>
</body>
</html>

Auf drei Wegen, und du willst alle drei kennen.

Von selbst. Passiert eine Weile nichts, räumt PHP die Datei irgendwann weg. Die Voreinstellung liegt bei 24 Minuten.

Weil der Browser zugeht. Das Cookie mit der Kennung ist ein Sitzungscookie ohne Laufzeit. Ist der Browser zu, ist es weg, und der Besucher findet seine Sitzung nicht mehr, auch wenn die Datei noch eine Weile daliegt.

Weil du es sagst. Das ist der Fall, der dich interessiert, und er braucht drei Schritte: $_SESSION = [] leert die Daten im laufenden Programm, setcookie() mit einem Zeitpunkt in der Vergangenheit nimmt dem Browser die Kennung wieder ab, und session_destroy() löscht die Datei. Wer nur session_destroy() aufruft, lässt dem Besucher eine Kennung, die auf nichts mehr zeigt. Das ist meistens harmlos und manchmal genau der Fehler, den man sucht.

Im dritten Beispiel siehst du alle drei nebeneinander. Klick auf „Sitzung beenden” und sieh in den Reiter Netzwerk: Dort steht der Klick als zwei Zeilen, erst GET /?ende=1 mit 302, unter den Antwort-Kopfzeilen Set-Cookie: PHPSESSID=deleted mit einem Ablaufdatum im Jahr 1970 und Location: /, darunter GET / mit 200 und einer frischen Kennung im Set-Cookie. Die Adresszeile zeigt danach wieder /, als wäre nichts gewesen; der Umweg steht nur dort. Genau diese Reihenfolge baust du in Lektion 12.5 als Abmelden ein.

Zum Mitnehmen

Eine Sitzung ist der Trick, mit dem fast jede Anmeldung im Web funktioniert, und er ist erstaunlich schlicht: Der Besucher bekommt eine Nummer, alles andere bleibt bei dir. Wer diesen Satz verstanden hat, versteht auch, warum in einer Sitzung liegen darf, was in einem Cookie nichts zu suchen hat.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.