7. Persistenz, Autorisierung, Cookies

7.2 Cookies

Neben localStorage und sessionStorage gibt es noch eine weitere Möglichkeit, Daten persistent zu speichern: Cookies. Diese funktionieren leicht anders (was womöglich historische Gründe hat). Zum ersten Rumspielen mit Cookies laden Sie sich bitte server.js und index.html herunter. Starten Sie den Server:
node server.js                        
server started on port 6024
und gehen im Browser auf localhost:6024. Öffnen Sie die Javascript-Konsole und tippen Sie document.cookie. Das sollte so aussehen:
document.cookie
''

Die lokale Variable document.cookie ist also der leere String. Klicken Sie nun den Knopf request cookie und schauen, welchen Wert die Variable jetzt hat. Bei mir:

document.cookie
'key_238=value_118'
. Drücken Sie den Knopf nochmal. Dann:
document.cookie
'key_238=value_118; key_83=value_840'

Was passiert hier? Die Variable document.cookie kann also, ähnlich der document.localStorage, Key-Value-Paare speichern. Allerdings ist sie kein Javascript-Dictionary, sondern ein String, der die Key-Value-Paare im Format key1=value1; key2=value2; key3=value3 speichert, also in einem einzigen String. Dieser Unterschied ist eher kosmetisch. Ein gewichtigerer Unterschied ist allerdings: die Cookies werden in diesem Beispiel rein serverseitig gestzt. In der Tat ist die Seite index.html rein statisch, enthält also kein Javascript. Cookies sind bereits in Http definiert. Die Idee ist, dass der Cookie-String bei jedem Http-Austausch (Request, Response) hin- und hergeschickt wird. Dies dient auch dazu, dass der Server den Client "erkennt", also verschiedene Clients auseinanderhalten kann (z.B. damit er Ihnen den Inhalt Ihrer Email-Inbox anzeigt und nicht meiner). Wie werden Cookies in Http behandelt?

Cookies in Http

Um zu verstehen, wie Cookies in Http übermittelt werden, beginnen wir mit einem Experiment. Wir belauschen wir einfach die Kommunikation zwischen dem Client (Browser) und server.js.

Demo. Laden Sie sich hierfür das Java-Programm ServerSniffer.java herunter. Öffnen Sie zwei Terminals. Auf dem ersten gehen Sie in das Verzeichnis von server.js, mit dem zweiten zu ServerSniffer.java. Dann tippen Sie in das erste

node server.js       
a server that sets random cookies                     
server started on port 6024
Der Server läuft nun. Zwischen Browser und Server schalten wir jetzt noch den Sniffer:
java ServerSniffer localhost 6024 6025
Der Sniffer wartet nun auf Port 6025 auf Verbindungen und leitet dann alle hereinkommenden Daten weiter nach localhost:6024; also zu unserem Http-Server. Geben Sie nun im Browser in der Adresszeile ein:
http://localhost:6025/                            

Im Browser können Sie nun die Ihnen bereits bekannte Html-Seite sehen. Gehen Sie in das Konsolenfenster von ServerSniffer und betrachten die Ausgabe. Da geschieht noch nichts ungewöhnliches:

...
GET / HTTP/1.1 // das ist der Http-Request
Host: localhost:6025
...
HTTP/1.1 200 OK // das ist der Http-Response
...
// Im Body vom Response steht dann die Html-Datei.

Interessanter wird es, wenn Sie in der Html-Seite den Knopf request cookie drücken:

...
GET /request-cookie HTTP/1.1 // das ist der Http-Request, der durch den Knopfdruck ausgelöst worden ist
Host: localhost:6025
...
HTTP/1.1 302 Found // das ist der Http-Response, der den Browser bittet, nun eine andere Location zu laden
X-Powered-By: Express
Set-Cookie: key_968=value_722; Path=/ hier wird ein Cookie gesetzt
Location: /index.html // das ist die Location, die der Browser nun bitte laden soll 
...
// Jetzt kommt wiederum ein GET-Request vom Browser auf /index.html und der Response vom Server.

Beachten Sie die entscheidende Zeile Set-Cookie im Http-Header. Mit dieser Zeile bittet der Server den Client, das Key-Value-Paar key_968=value_722 zu speichern.

Beenden Sie nun server.js und ServerSniffer.java und schließen das Tab im Browser. Starten Sie dann nun server.js und ServerSniffer.java neu, öffnen ein neues Tab, und laden abermals localhost:6025. Schauen Sie sich im Output vom ServerSniffer.java den GET-Request an:

...
GET / HTTP/1.1 // wurde durch Eingabe von localhost:6025 in der Adresszeile des Browsers ausgelöst
Host: localhost:6025                        
...
Cookie: key_968=value_722 // hier teilt der Client dem Server die gespeicherten Cookies mit
...

Die Cookies sind also ein String, der im Ping-Pong-Verfahren im Http-Header zwischen Server und Client hin- und hergeschickt wird.

Wie werden Cookies in Http gelöscht? Drücken Sie den Knopf delete cookies und betrachten Sie den Output von ServerSniffer.java:

...
GET /delete-all-cookies HTTP/1.1 // wurde durch Eingabe von localhost:6025 in der Adresszeile des Browsers ausgelöst
Host: localhost:6025                        
...
Cookie: key_968=value_722 // der Client schickt wie immer dem Server die Cookies
...
HTTP/1.1 302 Found
...
Set-Cookie: key_968=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT

Seltsamerweise gibt es in Http keinen Header Del-Cookie. Stattdessen wird das Cookie mit dem Schlüssel key_968 erneut gesetzt (mit leerem Value) und dem 1. Januar 1970 als Verfallsdatum. In der Tat: Sie können Cookies nicht "löschen", Sie können nur das Ablaufdatum auf "gestern" setzen.

Cookies im Server

Jetzt wissen wir also in der Theorie, wie Cookies in Http gehandhabt werden. Es ist einfach eine zusätzliche Zeile im Http-Header. In der Praxis ist das aber irrelevant. Da wollen Sie lieber wissen, wie man serverseitig in Node.js mit Cookies umgehen kann. Dieser Abschnitt ist somit auch spezifisch für node.js und express.js. Wir betrachten die Code-Ausschnitte aus server.js.

Cookies setzen in Node.js / express.js                        
app.get('/request-cookie', (req, res) => {
    var key = "key_" + randint(1000);
    var value = "value_" + randint(1000);
    console.log(`cookie is ${JSON.stringify(req.cookies)}`);
    console.log(`setting new cookie "${key}=${value}"`);
    res.cookie(key, value);
    return res.redirect("/index.html");
});

So einfach geht das. Mit res.cookie(key,value) in Zeile 26 setzt der Server das Cookie. Lesen geht ähnlich einfach:

Cookies lesen in Node.js / express.js
const cookieParser = require('cookie-parser');
...
app.use(cookieParser()); // sonst kann die App nicht den Cookie-String lesen
...
app.get('/show-cookies', (req,res) => {
    var responseString = 'Your cookies are: ';
    
    for (key in req.cookies) {
        var value = req.cookies[key];
        responseString += `key: ${key}, value: ${value} n`;
    }
    res.status(200);
    return res.send(responseString);
});

Mit cookie-parser wird req.cookies zu einem gewöhnlichen Javascript-Dictionary, mit dem Sie serverseitig arbeiten können. (Sie können das natürlich ausprobieren, indem Sie localhost:6025/show-cookies aufrufen. Es gibt einfach auf der index.html keinen Link, der diese Route aufruft.) Zum Schluss zeigen wir, wie man Cookies serverseitig in express.js löscht:

Cookies löschen in Node.js / express.js                        
app.get('/delete-all-cookies', (req,res) => {
    for (key in req.cookies) {
        res.clearCookie(key);
    }
    return res.redirect("/index.html");
});

Über die historische Kuriosität, dass man in Http Cookies nicht direkt löschen kann, müssen Sie sich also keine Gedanken machen. res.clearCookie(key) setzt für Sie das Verfallsdatum auf den 1. Januar 1970, was einem Löschen gleichkommt.

Cookies Client-seitig setzen

Öffnen Sie ein neues Tab und dort die Javascript-Konsole. Mit document.cookie = 'username=dominik'; können Sie Client-seitig mit Javascript Cookies setzen.
Übungsaufgabe Experimentieren Sie mit document.cookie = 'key1=value1'; in der Javascript-Konsole rum. Was geschieht? Was ist unintuitiv?

Übung Laden Sie 09-setcookie-getcookie.zip herunter und starten Sie die Anwendung. Sie können über die Maske nun Cookies setzen, bzw. der Post-Request den Server bitten, die Cookies für Sie zu setzen.

Fügen Sie dieser App eine delete-Funktion hinzu. In der Tabelle soll neben jedem Cookie ein delete-Link stehen. Wenn man auf diesen klickt, soll der Server per GET-Request gebeten werden, das Cookie zu löschen.

Zu keinem Zeitpunkt sollten Sie browserseitig auf document.cookie zugreifen.