Abschnitt 14 · Lektion 5
Abmelden
Abmelden sieht nach der einfachsten Route des ganzen Abschnitts aus: Cookie weg, fertig. Genau deshalb steht sie hier als eigene Lektion, denn dieses „fertig” ist falsch, und der Fehler ist in freier Wildbahn häufig.
Zwei Schritte, und nur der zweite zählt
Beim Abmelden passieren zwei Dinge. Der Browser wirft seinen Zettel weg, und der Server verwirft die Sitzung. Nur das zweite ist das Abmelden.
// Der Ausweis liegt beim Browser, die Sitzung beim Server. Wer nur den
// Ausweis wegwirft, hat den Server nicht gefragt.
const sitzungen = new Map([["ABC123", { nutzer: "anna@beispiel.de" }]]);
const oeffnet = (ausweis) => sitzungen.get(ausweis)?.nutzer ?? "nichts mehr";
// Anna meldet sich ab. Variante eins: nur das Cookie loeschen.
console.log("nur das Cookie geloescht");
console.log(" Annas Browser hat: keinen Ausweis mehr");
console.log(` eine Kopie oeffnet: ${oeffnet("ABC123")}`);
// Variante zwei: die Sitzung auf dem Server verwerfen.
sitzungen.delete("ABC123");
console.log("");
console.log("Sitzung auf dem Server verworfen");
console.log(" Annas Browser hat: keinen Ausweis mehr");
console.log(` eine Kopie oeffnet: ${oeffnet("ABC123")}`); Der Grund ist derselbe wie in 13.4: Der Ausweis liegt beim Besucher, die Sitzung liegt bei dir. Was beim Besucher liegt, kannst du nicht zurückholen, du kannst ihn nur bitten, es wegzuwerfen. Wer den Wert vorher irgendwo abgeschrieben hat, aus einem geteilten Rechner, aus einem Protokoll, aus einer Browser-Erweiterung, der kommt damit weiter hinein. Und zwar so lange, bis die Sitzung von selbst abläuft, also unter Umständen Wochen.
Ein sitzungen.delete(...) ist eine Zeile. Sie ist der Unterschied zwischen „der Besucher sieht die
Anmeldeseite” und „der Besucher ist abgemeldet”.
Nebenbei bekommst du damit eine Funktion geschenkt, nach der Nutzer öfter fragen: „Auf allen Geräten
abmelden”. Wenn deine Sitzungen serverseitig liegen und wissen, zu wem sie gehören, ist das ein
Durchlauf über die Sitzungen dieses Nutzers und ein delete je Treffer. Mit einem signierten Token
ohne Serverzustand geht genau das nicht, und das ist der Hauptgrund, warum dieser Kurs die Sitzung
nimmt.
Ein Cookie löscht man mit denselben Attributen
Der erste Schritt hat trotzdem seine Tücke. Es gibt keinen Befehl „lösch das Cookie”, sondern nur
noch eine Set-Cookie-Zeile, diesmal mit leerem Wert und einem Ablaufdatum in der Vergangenheit.
import express from "express";
const app = express();
// Gesetzt wird das Cookie mit einem eigenen Pfad.
app.get("/setzen", (req, res) => {
res.cookie("sid", "ABC123", { path: "/intern", httpOnly: true, sameSite: "lax" });
res.end();
});
// So nicht: geloescht wird ohne diesen Pfad. Fuer den Browser sind das
// zwei verschiedene Cookies, das erste bleibt also stehen.
app.get("/loeschen-falsch", (req, res) => {
res.clearCookie("sid");
res.end();
});
// So: dieselben Attribute wie beim Setzen.
app.get("/loeschen-richtig", (req, res) => {
res.clearCookie("sid", { path: "/intern", httpOnly: true, sameSite: "lax" });
res.end();
});
app.listen(3000); Ruf /setzen auf, dann /loeschen-falsch, dann wieder /setzen und /loeschen-richtig. Die
Unterschiede stehen in der Kopfzeile, also im Terminal:
curl -i http://localhost:3000/loeschen-falsch
curl -i http://localhost:3000/loeschen-richtig
Der Browser ordnet ein Cookie nach Name, Domain und Pfad zu. Passt eines davon nicht, hält er die
Löschanweisung für ein anderes Cookie, und das alte bleibt friedlich stehen. Deshalb gehören in
clearCookie dieselben Angaben wie in cookie. In unserer Aufgabe ist der Pfad überall der
Standard, sodass es reicht, an httpOnly und sameSite zu denken.
Abmelden ist ein POST
Bleibt die Frage nach der Methode. Abmelden verändert etwas, also gehört es nicht hinter ein GET. Das ist keine Formalie.
import express from "express";
const app = express();
const sitzungen = new Map([["ABC123", { nutzer: "anna@beispiel.de" }]]);
const stand = () => (sitzungen.has("ABC123") ? "steht noch" : "weg");
// So nicht: abmelden per GET. Jeder Aufruf der Adresse meldet ab, und
// ein Bild auf einer fremden Seite ist so ein Aufruf.
app.get("/abmelden-per-get", (req, res) => {
sitzungen.delete("ABC123");
res.type("text/plain").send("abgemeldet");
});
// So: abmelden nur per POST, und GET sagt ausdruecklich, dass es das
// nicht tut.
app.post("/logout", (req, res) => {
sitzungen.delete("ABC123");
res.type("text/plain").send("abgemeldet");
});
app.get("/logout", (req, res) => {
res.status(405).set("Allow", "POST").type("text/plain").send("Zum Abmelden bitte das Formular benutzen.");
});
app.listen(3000); Rechts steht die Abmeldung per GET, und sie funktioniert, obwohl niemand auf einen Knopf gedrückt hat. Genau das ist das Problem.
Ein GET löst jeder aus, der eine Adresse in eine Seite schreibt, und ein Bild-Element mit deiner
Abmelde-Adresse als Quelle ist genau das. Der Browser lädt es, schickt Annas Cookie mit, und Anna ist
weg. Für sich genommen ist der Schaden gering, sie ärgert sich und meldet sich neu an. Wichtig ist
das Muster dahinter, denn dieselbe fremde Seite kann auch auf „löschen”, „bestellen” oder
„überweisen” zeigen. Das hat einen Namen, CSRF, und SameSite aus 13.3 ist die Abwehr. In 15.4 kommt
das ausführlich.
Ein Detail im Beispiel gehört zum guten Ton: Wer /logout per GET aufruft, bekommt keinen
404, sondern 405 Method Not Allowed und eine Allow-Kopfzeile, die sagt, was ginge. 404 hieße
„gibt es nicht”, und das stimmt ja nicht.
Zum Mitnehmen
Das Cookie zu löschen fühlt sich nach Abmelden an und ist keins. Wer den Ausweis vorher kopiert hat, ist weiter drin. Zählen tut nur, was auf dem Server passiert.
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.