0%

Zustand und Identität · Abschlussprojekt

Abschlussprojekt: Projekt: Wer die Liste ändern darf

Deine Leseliste weiß jetzt, wer fragt. Eine Anmeldung prüft ein Passwort und startet eine Session in einem HttpOnly-, /me meldet den Benutzer der Session, und jeder Schreibzugriff braucht ein Token, das schreiben darf. Eines fehlt noch: Eine Session kann beginnen, aber nie enden. Dieses Projekt fügt POST /logout hinzu und prüft dann das ganze Kapitel gegen den Vertrag.

Was der Vertrag verlangt

Öffne SERVICE_CONTRACT.md und lies seinen Abschnitt „Who you are“ als Checkliste:

  • POST /login mit den Basic-Zugangsdaten eines bekannten Benutzers: 200, {"user": name} und set-cookie: session=<id>; HttpOnly; Max-Age=3600; Path=/. Alles andere: 401 mit der Basic-Challenge, ohne Cookie und ohne Session.

  • GET /me: 200, mit dem Benutzer der Session oder null.

  • POST /logout: 204 ohne Body und ohne content-length; die Session endet, und set-cookie lässt das Cookie ablaufen.

  • POST, PUT und DELETE auf Büchern: 401 mit www-authenticate: Bearer bei einem fehlenden, fehlerhaften oder unbekannten Token, 403 bei einem Token, das nur lesen darf, geprüft vor der Buch-ID und dem Body.

  • 405 mit allow: POST auf /login und /logout und allow: GET, HEAD auf /me.

  • Kein Antwort-Body enthält ein Passwort, ein Token oder eine Session-ID.

Jede Zeile außer der Abmeldung sollte bereits gelten. Starte das Szenario who des Hilfsprogramms, um zu sehen, wo du stehst:

python pl_http.py who

Es meldet sich als ana mit Basic-Zugangsdaten an, liest /me mit dem Cookie, das es bekommen hat, sendet POST /logout mit diesem Cookie und Content-Length: 0, liest /me noch einmal mit demselben alten Cookie und sendet zuletzt POST /books mit dem Lese-Token. Heute antwortet die Abmeldung mit HTTP/1.0 404 Not Found, und das zweite /me antwortet immer noch {"user": "ana"}: Die Session lebt.

Eine Session beenden

Abmelden braucht zwei Schritte, und sie sind nicht gleich wichtig.

Erstens entfernt der Server die Session aus state["sessions"]. Das ist der Schritt, der zählt. Sobald die ID weg ist, ist das Cookie wertlos: Jede Anfrage, die es trägt, von diesem Client oder von jemandem, der es kopiert hat, bekommt {"user": null}.

Zweitens bittet die Antwort den Client, das Cookie zu vergessen, mit einem set-cookie, dessen Max-Age 0 ist. Dein session_cookie schreibt das bereits: session_cookie("", max_age=0) ergibt session=""; HttpOnly; Max-Age=0; Path=/, wenn es SimpleCookie verwendet, das den leeren als "" schreibt. Ein schreibt session=; HttpOnly; Max-Age=0; Path=/. Beide Zeilen lassen das Cookie ablaufen. Ein Cookie-Speicher, der diese Zeile liest, verwirft sein Cookie session sofort. Aber Lektion 1 hat es gesagt: Jedes Attribut ist eine Bitte, die der Client ignorieren kann. Ein Server, der nur diese Zeile sendet und die Session behält, würde das alte Cookie für jeden funktionieren lassen, der eine Kopie behalten hat.

Um die Session zu finden, lies das Cookie session auf dieselbe Weise wie session_user und entferne es mit state["sessions"].pop(value, None). Mit einem Standardwert gibt pop diesen zurück, statt auszulösen, wenn der Schlüssel fehlt, und das passiert bei einer unbekannten oder bereits beendeten Session. Sich ohne Session abzumelden oder zweimal hintereinander ist kein Fehler: Der Client ist so oder so abgemeldet, also antwortet jedes Mal 204. Nichts auf diesem Server beendet eine Session nach Zeit, also ist die Abmeldung der einzige Weg, auf dem eine endet, bevor der Server anhält.

Meilensteine

  1. Füge in handle einen Zweig für /logout hinzu. Ein POST beendet die Session, die sein Cookie benennt, falls es eine gibt, und antwortet mit 204, einem leeren Body, ohne content-length und mit einem Header set-cookie aus session_cookie("", max_age=0). Jede andere bekommt not_allowed("POST").

  2. Führ python pl_http.py who noch einmal aus. Die Abmeldung antwortet jetzt mit HTTP/1.0 204 No Content, und das nächste /me mit dem alten Cookie antwortet {"user": null}. Das POST /books mit dem Lese-Token bekommt weiterhin HTTP/1.0 403 Forbidden. Unter der 204 gibt das Hilfsprogramm die Zeile aus, die das Cookie ablaufen lässt, hier so, wie SimpleCookie sie schreibt:

  set-cookie: session=""; HttpOnly; Max-Age=0; Path=/
  1. Du kannst tests/test_server.py einen Test für die Abmeldung hinzufügen. make_request nimmt ein von Headern, also kann ein Test ein Cookie senden, zum Beispiel:

def test_logout_ends_the_session():
    state = reading_server.sample_state()
    session_id = reading_server.start_session(state, "ana")
    cookie = {"cookie": "session=" + session_id}
    response = reading_server.handle(make_request("POST", "/logout", headers=cookie), state)
    assert response["status"] == 204
    me = reading_server.handle(make_request("GET", "/me", headers=cookie), state)
    assert json.loads(me["body"].decode("utf-8")) == {"user": None}
  1. Führ python -m pytest -q aus. Jeder Test sollte bestehen, die Tests eingeschlossen, die mit dem Kapitel gekommen sind.

python -m pytest -q

Was geprüft wird

Check my work prüft noch einmal das ganze Kapitel, mit Benutzern, Passwörtern, Tokens und Büchern, die es dir nicht gezeigt hat, und mit Session-IDs, die es selbst wählt, wie es die Prüfung in Lektion 2 getan hat. Es ruft handle und die Hilfsfunktionen direkt für Anmeldungen, /me, Abmeldung und jede Art von Schreibzugriff auf, vergleicht den Zustand vor und nach jeder Ablehnung und durchsucht jede Antwort nach den Zugangsdaten, die es gesendet hat. Es baut die Handler-Klasse deines Servers mit make_server, ohne einen zu öffnen, schickt eine Anmeldung und einen Schreibzugriff ohne Token als Bytes hindurch und liest die Zeilen set-cookie und www-authenticate, die zurückkommen. Zuletzt führt es deine Tests einmal aus, so wie sie sind; sie müssen alle bestehen, und keiner darf socket importieren. Das Szenario who in deinem ist zum Zuschauen da; nichts davon wird bewertet.

Drück dann Check my work. Deine sagt jetzt, wer sie ändern darf. Jeder Leser lädt allerdings bei jeder Frage noch die ganze Liste herunter, selbst wenn sich seit dem letzten Mal nichts geändert hat. Kapitel 8 bringt dem Server bei, „nichts hat sich geändert“ zu sagen, und dem Client, sich an das zu erinnern, was er schon hat.

Aufgabe

Füge reading_server.py POST /logout hinzu und sorg dafür, dass jede Regel von „Who you are“ in SERVICE_CONTRACT.md gilt.

  • POST /logout entfernt die Session, die sein session benennt, aus state["sessions"], falls es eine gibt, und antwortet mit 204, dem Body b"", ohne content-length und mit einem Header set-cookie aus session_cookie("", max_age=0). Ohne Cookie oder bei einer bereits beendeten Session antwortet es trotzdem mit 204. Jede andere auf /logout bekommt 405 mit allow: POST.

  • Das POST /logout eines Clients sendet keinen Body, also sendet es, wie jedes POST an diesen Server, content-length: 0; ohne das antwortet read_body mit 411.

  • Alles aus den Lektionen 2, 5 und 6 gilt weiterhin: Basic-Anmeldung mit einer Session in einem HttpOnly-Cookie oder 401 mit der Basic-Challenge, /me mit dem Benutzer oder null, durch Bearer-Token geschützte Schreibzugriffe mit 401 oder 403 je nach Ursache vor der Buch-ID und dem Body, die 405er und kein Passwort, kein Token und keine Session-ID in irgendeiner Antwort.

Führ python pl_http.py who im aus, um dich anzumelden, abzumelden und zu sehen, wie das alte Cookie aufhört zu funktionieren, und python -m pytest -q, um deine Tests auszuführen.

Drück dann Check my work. Mit Benutzern, Tokens, Büchern und Session-IDs, die du noch nicht gesehen hast, ruft es handle und die Hilfsfunktionen direkt für Anmeldungen, /me, Abmeldung und jede Art von Schreibzugriff auf, vergleicht den Zustand vor und nach jeder Ablehnung und durchsucht jede Antwort nach Zugangsdaten. Es baut die Handler-Klasse deines Servers mit make_server, ohne einen zu öffnen, und schickt eine Anmeldung und einen Schreibzugriff ohne Token als Bytes hindurch. Zuletzt führt es deine Tests einmal aus, so wie sie sind: Sie müssen alle bestehen, und keiner darf socket importieren. Nichts aus deinem Terminal wird bewertet.