Abschnitt 13 · Lektion 4
Sitzungen
Am Ende der letzten Lektion stand die Regel, dass nichts ins Cookie gehört, was der Besucher nicht ändern können soll. Das klingt nach einer harten Einschränkung, denn ein Warenkorb, ein Anmeldezustand und eine Bestellung fallen alle darunter. Die Lösung dreht den Gedanken einfach um.
Der Ausweis statt der Daten
Statt die Daten zum Browser zu schicken, bleibt beim Browser nur ein Schlüssel, und die Daten bleiben beim Server. Der Schlüssel heißt Sitzungsausweis, und die Sache heißt Sitzung.
import express from "express";
import { randomBytes } from "node:crypto";
const app = express();
// Die Daten liegen hier, beim Server. Im Cookie steht nur der
// Schluessel zu diesem Eintrag.
const sitzungen = new Map();
app.use((req, res, next) => {
const cookies = Object.fromEntries(
(req.headers.cookie ?? "")
.split(";")
.map((teil) => teil.trim().split("="))
.filter((paar) => paar.length === 2)
);
let sid = cookies.sid;
if (!sid || !sitzungen.has(sid)) {
sid = randomBytes(16).toString("hex");
sitzungen.set(sid, { besuche: 0 });
res.cookie("sid", sid, { httpOnly: true, sameSite: "lax" });
}
req.sitzung = sitzungen.get(sid);
next();
});
app.get("/zaehler", (req, res) => {
req.sitzung.besuche += 1;
res.type("text/plain").send(`Besuch ${req.sitzung.besuche}, ${sitzungen.size} Sitzung(en) im Speicher`);
});
app.listen(3000); Rechts steht der Zähler. Lade die Seite ein paarmal neu, er geht mit. Dein Browser schickt dabei denselben Ausweis mit, ohne dass du etwas dafür tust.
Die Middleware macht drei Dinge: Ausweis aus dem Cookie lesen, nachsehen, ob es dazu einen Eintrag
gibt, und andernfalls einen neuen anlegen und mitgeben. Danach hängt an req.sitzung ein Objekt, das
nur diesem Besucher gehört, und die Routen darunter merken von alledem nichts mehr.
Zwei Anfragen des Beispiels lohnen einen zweiten Blick. Mit demselben Ausweis kommt „Besuch 2” zurück, der Zähler liegt also wirklich auf dem Server. Und mit einem erfundenen Ausweis beginnt es wieder bei eins, statt eine fremde Sitzung zu öffnen.
Wie ein Ausweis aussehen muss
Das ist die Stelle, an der man am meisten falsch machen kann, denn ein falscher Ausweis sieht genauso aus wie ein richtiger und funktioniert im Alltag genauso gut.
import { randomBytes } from "node:crypto";
// So nicht: ein Ausweis, den man hochzaehlen kann.
let naechste = 1;
const sitzungen = new Map();
function schlechterAusweis() {
const sid = `sitzung-${naechste}`;
naechste += 1;
return sid;
}
for (const name of ["Anna", "Bert"]) {
const sid = schlechterAusweis();
sitzungen.set(sid, { korb: `Warenkorb von ${name}` });
console.log(`${name} bekommt ${sid}`);
}
// Bert sieht seinen eigenen Ausweis und probiert die Nummer davor.
const geraten = "sitzung-1";
console.log(`Bert probiert ${geraten} und sieht: ${sitzungen.get(geraten).korb}`);
console.log(`Laenge fortlaufend: ${schlechterAusweis().length}`);
console.log(`Laenge aus Zufall: ${randomBytes(16).toString("hex").length}`); Wer Sitzungen durchnummeriert, verschenkt sie. Bert braucht keine Lücke und kein Werkzeug, er subtrahiert eins von seiner eigenen Nummer und sieht Annas Warenkorb.
Math.random() ist übrigens auch keine Lösung. Es ist nicht dafür gebaut, unvorhersagbar zu sein,
sondern dafür, schnell gleichverteilte Zahlen zu liefern, und aus ein paar beobachteten Werten lässt
sich der Rest berechnen. Nimm randomBytes aus node:crypto. 16 Bytes ergeben als Hex-Text 32
Zeichen, und die rät niemand.
Wo die Daten liegen
In den Beispielen ist es eine Map, und für diesen Kurs ist das genau richtig: Sie zeigt ohne
Umschweife, dass die Daten im Prozess liegen.
Für den Betrieb hat das zwei Haken. Ein Neustart deines Servers leert die Map und meldet damit alle
ab, und bei zwei laufenden Prozessen hinter einem Verteiler findet die zweite Anfrage die Sitzung der
ersten nicht. Deshalb liegen Sitzungen in echten Anwendungen in einer Datenbank oder in einem
Zwischenspeicher wie Redis. Am Ablauf ändert das nichts, nur die drei Zeilen um sitzungen.get und
sitzungen.set sehen dann anders aus.
Ablaufen und aufräumen
Eine Map, in die nur eingetragen wird, wächst. Jeder Besucher, der einmal vorbeischaut und nie wiederkommt, hinterlässt einen Eintrag, und niemand nimmt ihn je heraus.
const ABLAUF_MS = 30 * 60 * 1000;
const jetzt = Date.now();
const sitzungen = new Map([
["gerade eben", { besuche: 2, zuletzt: jetzt }],
["vor zehn Minuten", { besuche: 5, zuletzt: jetzt - 10 * 60 * 1000 }],
["vor zwei Stunden", { besuche: 9, zuletzt: jetzt - 120 * 60 * 1000 }],
]);
console.log(`vorher im Speicher: ${sitzungen.size}`);
function holen(sid) {
const sitzung = sitzungen.get(sid);
if (!sitzung) return null;
// Zu lange her: die Sitzung gilt nicht mehr und fliegt gleich hier raus.
if (jetzt - sitzung.zuletzt > ABLAUF_MS) {
sitzungen.delete(sid);
return null;
}
sitzung.zuletzt = jetzt;
return sitzung;
}
for (const sid of [...sitzungen.keys()]) {
console.log(`${sid.padEnd(18)} ${holen(sid) ? "gilt noch" : "abgelaufen, neue Sitzung noetig"}`);
}
console.log(`nachher im Speicher: ${sitzungen.size}`); Deshalb merkt sich jede Sitzung, wann sie zuletzt benutzt wurde, und wer zu lange weg war, bekommt eine neue. Das ist nicht nur Speicherhygiene, sondern auch eine Sicherheitsmaßnahme: Ein Ausweis, der ewig gilt, ist ewig brauchbar, falls ihn doch einmal jemand mitliest.
Die Alternative ohne Serverzustand
Es gibt einen zweiten Weg, und du wirst ihm begegnen: Die Daten wandern doch zum Browser, aber der Server unterschreibt sie, sodass jede Änderung auffällt. Signierte Token funktionieren so.
Der Vorteil ist, dass der Server nichts behalten muss. Der Nachteil zeigt sich beim Abmelden: Ein Token, das der Server nicht kennt, kann er auch nicht zurücknehmen. Es gilt bis zu seinem Ablaufdatum weiter, und wer es hat, ist bis dahin angemeldet. Wer das beheben will, führt eine Liste zurückgenommener Token, und dann ist der Serverzustand wieder da, nur umständlicher.
Für diesen Kurs bleibt es deshalb bei der Sitzung. Sie ist der Weg, der beim Abmelden einfach
sitzungen.delete(sid) heißt.
Zum Mitnehmen
Im Cookie steht nur ein Ausweis, die Daten bleiben beim Server. Deshalb kann der Besucher sie nicht umschreiben, und deshalb muss der Ausweis unratbar sein.
Jetzt du
Basis Konto, kostenlosZu 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.