Abschnitt 13 · Lektion 3
Cookies
Deine Weiterleitung aus 13.2 hat eine Frage offengelassen: Wo legt der Server etwas ab, das nur diesen einen Besucher betrifft? Zwei Anfragen desselben Menschen sehen für den Server nämlich aus wie die Anfragen zweier Fremder. HTTP behält nichts.
Der Zettel, den der Browser verwahrt
Ein Cookie ist die einfachste Antwort darauf. Der Server hängt an eine Antwort die Kopfzeile
Set-Cookie, der Browser merkt sich das Paar, und ab da schickt er es bei jeder weiteren Anfrage an
dieselbe Adresse in der Kopfzeile Cookie wieder mit.
import express from "express";
const app = express();
app.get("/einstellen", (req, res) => {
res.cookie("thema", "dunkel", { httpOnly: true, sameSite: "lax" });
res.end();
});
app.get("/", (req, res) => {
// Roh kommt alles in einer einzigen Kopfzeile an.
console.log(`req.headers.cookie: ${req.headers.cookie}`);
const cookies = Object.fromEntries(
(req.headers.cookie ?? "")
.split(";")
.map((teil) => teil.trim().split("="))
.filter((paar) => paar.length === 2)
);
console.log(`zerlegt: ${JSON.stringify(cookies)}`);
console.log(`cookies.thema: ${cookies.thema}`);
res.end();
});
app.listen(3000); Setzen übernimmt res.cookie(name, wert, optionen). Beim Lesen zeigt sich, wie unspektakulär die
Sache ist: req.headers.cookie ist eine Zeichenkette mit allen Cookies, getrennt durch
Strichpunkte. Ein split und ein trim, und du hast ein Objekt. Es gibt Pakete, die dir diese vier
Zeilen abnehmen, aber es lohnt sich, sie einmal gesehen zu haben: Ein Cookie ist nichts Magisches,
sondern Text in einer Kopfzeile.
Beachte, dass in der zweiten Anfrage drei Cookies stehen und nur eines davon von dir ist. Ein Browser sammelt sie über die Zeit, und deine Route muss sich das richtige heraussuchen statt die Zeile als Ganzes zu vergleichen.
Im Beispiel siehst du es: Die zweite Anfrage musste die Kopfzeile von Hand mitbringen, weil fetch
kein Gedächtnis hat. Ein Browser hat eins, und was der Reiter „Browser” zeigt, ist ein echter
Browser.
import express from "express";
const app = express();
const THEMEN = { hell: ["#ffffff", "#111111"], dunkel: ["#111111", "#f5f5f5"] };
function leseCookies(kopf) {
return Object.fromEntries(
(kopf ?? "")
.split(";")
.map((teil) => teil.trim().split("="))
.filter((paar) => paar.length === 2)
);
}
app.get("/einstellen", (req, res) => {
const thema = req.query.thema;
if (THEMEN[thema]) res.cookie("thema", thema, { httpOnly: true, sameSite: "lax" });
res.redirect(303, "/");
});
app.get("/", (req, res) => {
const cookies = leseCookies(req.headers.cookie);
const thema = THEMEN[cookies.thema] ? cookies.thema : "hell";
const [grund, schrift] = THEMEN[thema];
res.send([
"<!doctype html>",
'<meta charset="utf-8">',
"<title>Thema</title>",
`<body style="background:${grund};color:${schrift};font-family:sans-serif">`,
`<h1>Aktives Thema: ${thema}</h1>`,
`<p>Dein Browser hat geschickt: <code>${req.headers.cookie ?? "gar nichts"}</code></p>`,
'<p><a href="/einstellen?thema=hell">hell</a> · <a href="/einstellen?thema=dunkel">dunkel</a></p>',
].join("\n"));
});
app.listen(process.env.PORT ?? 3000); Klick auf „dunkel”. Die Seite wird dunkel, und darunter steht, was dein Browser geschickt hat. Lad sie über den Knopf Seite neu laden noch einmal, sie bleibt dunkel. Genau das ist der ganze Punkt eines Cookies, und es ist die erste Stelle in diesem Kurs, an der du ihn nicht nachstellen musst.
Alles Weitere steht in derselben Zeile
Was Set-Cookie außer Name und Wert noch trägt, sind Anweisungen an den Browser, wie er mit dem
Zettel umgehen soll.
import express from "express";
const app = express();
const faelle = [
["ohne Attribute ", {}],
["HttpOnly, SameSite ", { httpOnly: true, sameSite: "lax" }],
["dazu Secure, Strict ", { httpOnly: true, secure: true, sameSite: "strict" }],
["mit einer Stunde ", { httpOnly: true, sameSite: "lax", maxAge: 3600000 }],
];
faelle.forEach(([, optionen], i) => {
app.get(`/fall-${i}`, (req, res) => {
res.cookie("thema", "dunkel", optionen);
res.end();
});
});
app.listen(3000); Diese Antworten haben keinen Inhalt, ihr Inhalt ist die Kopfzeile. Deshalb gehören sie ins Terminal:
curl -i http://localhost:3000/fall-0
curl -i http://localhost:3000/fall-3
Vier Angaben lohnen sich zu kennen, und jede wehrt etwas Bestimmtes ab.
HttpOnly nimmt dem JavaScript im Browser den Zugriff. document.cookie sieht dieses Cookie
dann nicht mehr. Wer über eine Lücke fremdes JavaScript auf deiner Seite ausführen kann, kommt damit
trotzdem nicht an den Zettel heran. Das ist die wichtigste der vier.
Secure heißt: nur über HTTPS mitschicken. Ohne diese Angabe wandert das Cookie auch über eine
unverschlüsselte Verbindung, wo jeder im selben WLAN mitliest. In der Sandbox dieses Kurses läuft
alles über einfaches HTTP, deshalb bleibt sie hier aus.
SameSite=Lax sorgt dafür, dass der Browser das Cookie nicht mitschickt, wenn eine fremde
Seite eine Anfrage an deinen Server auslöst. Ohne diese Angabe reicht ein Formular auf einer
beliebigen anderen Seite, um im Namen deines eingeloggten Besuchers etwas auszulösen. Lax ist der
brauchbare Mittelweg, Strict ist strenger und sperrt auch normale Links von außen aus.
Max-Age gibt eine Lebensdauer in Sekunden. Ohne die Angabe ist es ein Sitzungscookie und
verschwindet, wenn der Browser zugeht. Interessant am letzten Fall ist die Reihenfolge: Express
schreibt Max-Age und ein daraus errechnetes Expires in die Zeile, und beide stehen vor
HttpOnly. Wer eine solche Kopfzeile vergleicht, vergleicht also je nach Fall verschiedene Zeilen.
Der Zettel liegt beim Besucher
Und damit zum wichtigsten Satz der Lektion.
import express from "express";
const app = express();
// So nicht: die Berechtigung steht im Cookie, und das Cookie gehoert
// dem Besucher.
app.get("/intern", (req, res) => {
const cookies = Object.fromEntries(
(req.headers.cookie ?? "")
.split(";")
.map((teil) => teil.trim().split("="))
.filter((paar) => paar.length === 2)
);
if (cookies.rolle === "admin") {
return res.type("text/plain").send("Willkommen im Adminbereich");
}
res.status(403).type("text/plain").send("Kein Zutritt");
});
app.listen(3000); Rechts steht der Normalfall. Jetzt schick dir selbst eine Rolle mit, die dir niemand gegeben hat:
curl -H "Cookie: rolle=admin" http://localhost:3000/intern
Der Server glaubt, was im Cookie steht, und im Cookie steht, was der Besucher hineinschreibt. Ein Cookie zu ändern braucht keine Werkzeuge und kein Wissen, die Entwicklerkonsole jedes Browsers hat eine Liste mit Bearbeitenknopf.
Ein Cookie mit rolle=admin ist keine Berechtigung, sondern eine Einladung. Dasselbe gilt für
preis=0, rabatt=90 und nutzer_id=1. Was hinein darf, ist alles, dessen Fälschung dir egal sein
kann: eine Themenwahl, eine Spracheinstellung, ein weggeklickter Hinweis.
Für alles andere gibt es zwei Wege. Entweder du unterschreibst den Inhalt, sodass eine Änderung auffällt, oder du legst gar keine Daten hinein, sondern nur einen Ausweis und behältst die Daten beim Server. Der zweite Weg ist der übliche, und er ist die nächste Lektion.
Zum Mitnehmen
Ein Cookie ist ein Zettel, den der Server dem Browser mitgibt und bei jeder weiteren Anfrage zurückbekommt. Der Browser verwahrt ihn, also darf nichts darauf stehen, was der Besucher nicht ändern können soll.
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 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.