Zustand und Identität · Übung
Führe eine Session auf dem Server
Ein session des Hilfsprogramms:
python pl_http.py session
Es sendet vier Anfragen: GET /me, dann POST /login, dann noch einmal GET /me mit dem Cookie, das aus der Zeile set-cookie der Anmeldeantwort kopiert wurde, falls es eine gibt, und zuletzt GET /me mit cookie: session=not-a-real-id. Heute ist jede Antwort HTTP/1.0 404 Not Found mit {"error": "nothing is at this path"}, weil noch keiner der beiden Pfade existiert.
Sieh dir das POST /login an, das das Hilfsprogramm sendet:
client -> server b'POST /login HTTP/1.1\r\nHost: books.test\r\nContent-Length: 0\r\n\r\n' (61 bytes)
Es hat keinen Body und sagt trotzdem Content-Length: 0. Das ist die Regel aus Kapitel 5, Lektion 1: Dein read_body beantwortet jedes POST ohne Länge mit 411, bevor handle sich überhaupt den Pfad ansieht, also sagt ein POST, das nichts zu senden hat, genau das.
Behalte die Fakten, gib einen Schlüssel heraus
Der verlockende Entwurf steckt den Benutzer in das Cookie: set-cookie: user=guest. Das funktioniert, bis ein Client stattdessen cookie: user=ana sendet. Ein Cookie ist Text, den der Client speichert und zurücksendet, und der Client kann ihn nach Belieben ändern, also kann nichts darin als Tatsache gelten.
Deshalb bleiben die Fakten auf dem Server. Wenn sich jemand anmeldet, denkt sich der Server eine neue zufällige ID aus, speichert unter dieser ID in state["sessions"], wer sich angemeldet hat, und gibt dem Client in einem Cookie nur die ID. Wenn eine Anfrage mit diesem Cookie zurückkommt, schlägt der Server die ID nach:
client: cookie: session=K7f...Qw
|
v
server: state["sessions"]
"K7f...Qw": {"user": "ana"}
"p2Z...8c": {"user": "ben"}
Die ID zeigt auf eine Zeile, und die Zeile sagt, wer der Benutzer ist. Das Cookie zu ändern bringt einem Client nichts, außer er errät die ID einer anderen Zeile. Dieser Eintrag auf dem Server ist eine Session, und die ID ist ihre Session-ID. sample_state() gibt jetzt dafür ein leeres "sessions" zurück, neben den Benutzern und Tokens, die spätere Lektionen verwenden.
Wo hält dieser Server fest, wer angemeldet ist?
Eine ID, die niemand erraten kann
Solange eine Session lebt, funktioniert ihre ID wie ein Passwort: Jeder, der cookie: session=<that id> sendet, ist für den Server dieser Benutzer. Also muss die ID unmöglich zu erraten sein. Pythons secrets erzeugt
secrets.token_urlsafe(16) nimmt 16 zufällige Bytes vom - und _. Sechzehn Bytes sind 128 zufällige Bits, viel zu viele Werte, um sie einzeln durchzuprobieren. Das Modul random, das du vielleicht von Spielen und Simulationen kennst, ist hier das falsche Werkzeug: Es ist darauf gebaut, schnell und wiederholbar zu sein, nicht unvorhersehbar. Verwende random für Simulationen und secrets für alles, was ein Angreifer nicht erraten darf.
Warum nummeriert man Sessions nicht 1, 2, 3?
start_session(state, user) erledigt diese Arbeit: ein Aufruf von secrets.token_urlsafe(16), über das Modul geschrieben als secrets.token_urlsafe, dann wird {"user": user} unter der neuen ID in state["sessions"] gespeichert und die ID zurückgegeben.
Das Cookie schreiben und lesen
session_cookie(session_id, *, max_age=SESSION_AGE) gibt den Wert des Headers set-cookie zurück: session=<id>; HttpOnly; Max-Age=<max_age>; Path=/. Du kannst ihn mit http.cookies.SimpleCookie bauen, wie in Lektion 1, indem du httponly, path und max-age setzt und OutputString() zurückgibst, oder mit einem token_urlsafe braucht keine Anführungszeichen, also schreiben beide Wege denselben Text. SESSION_AGE ist 3600, eine Stunde, und das Schlüsselwortargument lässt Lektion 7 eine 0 übergeben.
session_user(request, state) liest es zurück. request["headers"].get("cookie", []) ist eine cookie-Werte der Anfrage, meistens einer. Für jeden Wert liefert http.cookies.SimpleCookie(value).get("session") das Cookie session oder None, wenn die Zeile keins hat. Schlag sein .value mit .get in state["sessions"] nach und gib den gespeicherten Benutzer zurück, oder None, wenn kein Wert eine lebende Session benennt.
Was gibt das aus?
import http.cookies
c = http.cookies.SimpleCookie("theme=dark; session=abc")
print(c["session"].value, len(c))
SimpleCookie hat eine scharfe Kante. Wenn ein Stück einer Zeile Cookie fehlerhaft ist, gibt es überhaupt keine Cookies zurück, ohne Meldung: session=abc; bad cookie here; x=1 ergibt ein leeres Ergebnis. Ein kaputtes Cookie aus einem anderen Teil einer Website würde dann die Session verdecken. Ein selbst geschriebener Parser könnte nachsichtiger sein; diese Lektion verlangt keinen.
Was /me antwortet
GET /me antwortet immer mit 200: {"user": "guest"} bei einer lebenden Session, {"user": null} ohne Cookie, bei einer unbekannten ID oder einer beendeten Session. json.dumps schreibt Pythons None als null. Manche APIs antworten hier stattdessen mit 401. Eine 401 muss aber in einem Header WWW-Authenticate einen Anmeldeweg nennen, den der Client unter dieser /login, wo es ein Verfahren gibt.
Deine Aufgabe
In reading_server.py:
Schreib
start_session(state, user).Schreib
session_cookie(session_id, *, max_age=SESSION_AGE).Schreib
session_user(request, state).Füge in
handleeinen Zweig für den Pfad/loginhinzu: EinPOSTstartet eine Session für"guest"und antwortet mit 200,{"user": "guest"}und einem Headerset-cookieaussession_cookie; jede anderebekommt not_allowed("POST"). Füge einen Zweig für/mehinzu:GETundHEADantworten mit 200 und{"user": ...}aussession_user; jede andere Methode bekommtnot_allowed("GET, HEAD").
Bis Lektion 5 ist jede Anmeldung guest, egal was sie sendet: Es gibt noch keine Zugangsdaten zu prüfen.
Starte das Szenario noch einmal:
python pl_http.py session
Das erste GET /me antwortet jetzt {"user": null}. Die Anmeldung antwortet mit 200, und unter ihrer Antwort gibt das Hilfsprogramm die Zeile set-cookie aus: session=, dann 22 Zeichen, die sich bei jedem Lauf ändern, dann HttpOnly; Max-Age=3600; Path=/. Die dritte Anfrage trägt dieses Cookie und bekommt {"user": "guest"}; die ausgedachte ID bekommt {"user": null}.
Die Sessions leben im Zustands-Dictionary, also halten sie so lange wie ein Lauf des Hilfsprogramms. Max-Age bittet den Client, das Cookie nach einer Stunde zu vergessen, aber nichts auf diesem Server beendet eine Session nach Zeit: Er liest keine Uhr. Lektion 7 beendet Sessions auf Anfrage.
Drück dann Check my work. Der Server gibt jetzt ein Cookie heraus und erkennt es wieder, aber das Hilfsprogramm musste das Cookie von Hand in die nächste Anfrage kopieren. In der nächsten Lektion tut ein Client das selbst.
Aufgabe
Führe in reading_server.py eine Anmelde-Session auf dem Server.
start_session(state, user)erzeugt eine neue ID mitsecrets.token_urlsafe(16), über dasgeschrieben ( import secretssteht bereits oben;secrets.token_hex(16)wird auch akzeptiert, ebenso jede Größe ab 16 Bytes), speichert{"user": user}darunter instate["sessions"]und gibt die ID zurück.session_cookie(session_id, *, max_age=SESSION_AGE)gibt denset-cookie-session=<id>; HttpOnly; Max-Age=<max_age>; Path=/zurück, mit den Attributen in beliebiger Reihenfolge und ohneSecure, wie Lektion 1 erklärt hat. Bau ihn mithttp.cookies.SimpleCookieoder einem. session_user(request, state)liest dassessionaus dem Headercookieder Anfrage mithttp.cookies.SimpleCookieund gibt den Benutzer dieser Session zurück, oderNone, wenn es kein Cookie, kein Cookiesessionoder keine Session unter seinem Wert gibt.In
handle:POST /loginstartet eine Session für"guest"und antwortet mit 200,{"user": "guest"}und einem Headerset-cookieaussession_cookie; jede andereauf /loginbekommt 405 mitallow: POST.GET /meundHEAD /meantworten mit 200 und{"user": name}, oder{"user": null}ohne lebende Session; jede andere Methode bekommt 405 mitallow: GET, HEAD. Jede andere Route behält ihre Antwort.Die Session-ID erscheint nie in einem Antwort-Body.
Führ python pl_http.py session im set-cookie und die drei /me-Antworten.
Drück dann Check my work. Mit reading_server.secrets, ersetzt durch einen Ersatz, der die angefragte Größe aufzeichnet und IDs zurückgibt, die du noch nicht gesehen hast, prüft es, dass start_session die Session unter genau dieser ID speichert und dass das Cookie und handle sie verwenden. Es parst dein Cookie mit http.cookies.SimpleCookie und prüft den Wert, HttpOnly, Path=/ und Max-Age, einschließlich max_age=0. Es ruft handle mit einem Zustand auf, den du noch nicht gesehen hast: eine Anmeldung, /me mit dem Cookie allein, neben einem anderen Cookie in beiden Reihenfolgen, mit einer unbekannten ID, ohne Cookie und mit einem Cookie namens sessionx, HEAD /me und die 405er. Zuletzt startet es mit dem echten secrets 40 Sessions auf einem Zustand und auf frischen Zuständen und prüft, dass jede ID verschieden und mindestens 22 Zeichen lang ist. Nichts aus deinem Terminal wird bewertet.