Django-, Flask- und Redis-Tutorial: Sitzungsverwaltung von Webanwendungen zwischen Python-Frameworks
Veröffentlicht: 2022-03-11Django versus Flask: Wenn Django die falsche Wahl ist
Ich liebe und verwende Django in vielen meiner persönlichen und Kundenprojekte, hauptsächlich für eher klassische Webanwendungen und solche, die relationale Datenbanken beinhalten. Django ist jedoch keine Wunderwaffe.
Django ist konstruktionsbedingt sehr eng mit seinem ORM, dem Template-Engine-System und dem Einstellungsobjekt gekoppelt. Außerdem ist es kein neues Projekt: Es trägt viel Gepäck, um abwärtskompatibel zu bleiben.
Einige Python-Entwickler sehen darin ein großes Problem. Sie sagen, dass Django nicht flexibel genug ist und vermeiden es, wenn möglich, und verwenden stattdessen ein Python-Microframework wie Flask.
Diese Meinung teile ich nicht. Django ist großartig, wenn es am richtigen Ort und zur richtigen Zeit verwendet wird, auch wenn es nicht in jede Projektspezifikation passt. Wie das Mantra sagt: „Verwenden Sie das richtige Werkzeug für den Job“.
(Selbst wenn es nicht der richtige Ort und die richtige Zeit ist, kann das Programmieren mit Django manchmal einzigartige Vorteile haben.)
In einigen Fällen kann es in der Tat sinnvoll sein, ein leichteres Framework (wie Flask) zu verwenden. Oft beginnen diese Mikroframeworks zu glänzen, wenn Sie erkennen, wie einfach sie zu hacken sind.
Mikroframeworks zur Rettung
In einigen meiner Kundenprojekte haben wir darüber gesprochen, Django aufzugeben und zu einem Mikroframework zu wechseln, typischerweise, wenn die Kunden einige interessante Dinge tun wollen (in einem Fall zum Beispiel ZeroMQ in das Anwendungsobjekt einbetten) und das Projekt Ziele scheinen mit Django schwieriger zu erreichen zu sein.
Allgemeiner finde ich Flask nützlich für:
- Einfache REST-API-Backends
- Anwendungen, die keinen Datenbankzugriff erfordern
- NoSQL-basierte Web-Apps
- Web-Apps mit sehr spezifischen Anforderungen, wie z. B. benutzerdefinierte URL-Konfigurationen
Gleichzeitig erforderte unsere App eine Benutzerregistrierung und andere allgemeine Aufgaben, die Django vor Jahren gelöst hat. Aufgrund seines geringen Gewichts wird Flask nicht mit demselben Toolkit geliefert.
Es stellte sich die Frage: Ist Django ein Alles-oder-Nichts-Deal? Sollten wir es komplett aus dem Projekt streichen oder können wir lernen, es mit der Flexibilität anderer Mikroframeworks oder traditioneller Frameworks zu kombinieren? Können wir die Teile auswählen, die wir verwenden möchten, und auf andere verzichten?
Können wir das Beste aus beiden Welten haben? Ich sage ja, besonders wenn es um das Session Management geht.
(Ganz zu schweigen davon, dass es viele Projekte für Django-Freiberufler gibt.)
Jetzt das Python-Tutorial: Teilen von Django-Sitzungen
Das Ziel dieses Beitrags ist es, die Aufgaben der Benutzerauthentifizierung und -registrierung an Django zu delegieren, Redis jedoch zu verwenden, um Benutzersitzungen mit anderen Frameworks zu teilen. Ich kann mir ein paar Szenarien vorstellen, in denen so etwas nützlich wäre:
- Sie müssen eine REST-API separat von Ihrer Django-App entwickeln, möchten aber Sitzungsdaten freigeben.
- Sie haben eine bestimmte Komponente, die möglicherweise später ersetzt oder aus irgendeinem Grund skaliert werden muss, und benötigen weiterhin Sitzungsdaten.
Für dieses Tutorial verwende ich Redis, um Sitzungen zwischen zwei Frameworks (in diesem Fall Django und Flask) zu teilen. Im aktuellen Setup verwende ich SQLite zum Speichern von Benutzerinformationen, aber Sie können Ihr Back-End bei Bedarf an eine NoSQL-Datenbank (oder eine SQL-basierte Alternative) binden.
Sitzungen verstehen
Um Sitzungen zwischen Django und Flask auszutauschen, müssen wir ein wenig darüber wissen, wie Django seine Sitzungsinformationen speichert. Die Django-Dokumentation ist ziemlich gut, aber ich werde der Vollständigkeit halber einige Hintergrundinformationen liefern.
Session-Management-Varietäten
Im Allgemeinen haben Sie zwei Möglichkeiten, die Sitzungsdaten Ihrer Python-App zu verwalten:
Cookie-basierte Sitzungen : In diesem Szenario werden die Sitzungsdaten nicht in einem Datenspeicher im Back-End gespeichert. Stattdessen wird es serialisiert, signiert (mit einem SECRET_KEY) und an den Client gesendet. Wenn der Client diese Daten zurücksendet, wird ihre Integrität auf Manipulation überprüft und sie werden erneut auf dem Server deserialisiert.
Speicherbasierte Sitzungen : In diesem Szenario werden die Sitzungsdaten selbst nicht an den Client gesendet. Stattdessen wird nur ein kleiner Teil (ein Schlüssel) gesendet, um die Identität des aktuellen Benutzers anzugeben, der im Sitzungsspeicher gespeichert ist.
In unserem Beispiel interessiert uns eher letzteres Szenario: Wir möchten, dass unsere Sitzungsdaten im Backend gespeichert und dann in Flask überprüft werden. Das Gleiche könnte mit der ersten Methode gemacht werden, aber wie die Django-Dokumentation erwähnt, gibt es einige Bedenken hinsichtlich der Sicherheit der ersten Methode.
Der allgemeine Arbeitsablauf
Der allgemeine Arbeitsablauf der Sitzungsbehandlung und -verwaltung ähnelt diesem Diagramm:
Gehen wir die Sitzungsfreigabe etwas detaillierter durch:
Wenn eine neue Anfrage eingeht, besteht der erste Schritt darin, sie durch die registrierte Middleware im Django-Stack zu senden. Wir interessieren uns hier für die
SessionMiddleware
-Klasse, die, wie zu erwarten, mit Sitzungsverwaltung und -behandlung zusammenhängt:class SessionMiddleware(object): def process_request(self, request): engine = import_module(settings.SESSION_ENGINE) session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None) request.session = engine.SessionStore(session_key)
In diesem Snippet schnappt sich Django die registrierte
SessionEngine
(dazu kommen wir gleich), extrahiert denSESSION_COOKIE_NAME
aus derrequest
(standardmäßigsessionid
) und erstellt eine neue Instanz der ausgewähltenSessionEngine
, um die Sitzungsspeicherung zu handhaben.
Später (nachdem die Benutzeransicht verarbeitet wurde, sich aber noch im Middleware-Stapel befindet) ruft die Sitzungs-Engine ihre save-Methode auf, um alle Änderungen im Datenspeicher zu speichern. (Während der Behandlung von Ansichten hat der Benutzer möglicherweise einige Dinge innerhalb der Sitzung geändert, z. B. durch Hinzufügen eines neuen Werts zum Sitzungsobjekt mit
request.session
.) Dann wird derSESSION_COOKIE_NAME
an den Client gesendet. Hier die vereinfachte Version:def process_response(self, request, response): .... if response.status_code != 500: request.session.save() response.set_cookie(settings.SESSION_COOKIE_NAME, request.session.session_key, max_age=max_age, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, path=settings.SESSION_COOKIE_PATH, secure=settings.SESSION_COOKIE_SECURE or None, httponly=settings.SESSION_COOKIE_HTTPONLY or None) return response
Wir sind besonders an der SessionEngine
-Klasse interessiert, die wir durch etwas zum Speichern und Laden von Daten in und aus einem Redis-Back-End ersetzen werden.
Glücklicherweise gibt es einige Projekte, die dies bereits für uns erledigen. Hier ist ein Beispiel aus redis_sessions_fork . Achten Sie genau auf die save
und load
, die so geschrieben sind, dass sie die Sitzung (jeweils) in und aus Redis speichern und laden:
class SessionStore(SessionBase): """ Redis session back-end for Django """ def __init__(self, session_key=None): super(SessionStore, self).__init__(session_key) def _get_or_create_session_key(self): if self._session_key is None: self._session_key = self._get_new_session_key() return self._session_key def load(self): session_data = backend.get(self.session_key) if not session_data is None: return self.decode(session_data) else: self.create() return {} def exists(self, session_key): return backend.exists(session_key) def create(self): while True: self._session_key = self._get_new_session_key() try: self.save(must_create=True) except CreateError: continue self.modified = True self._session_cache = {} return def save(self, must_create=False): session_key = self._get_or_create_session_key() expire_in = self.get_expiry_age() session_data = self.encode(self._get_session(no_load=must_create)) backend.save(session_key, expire_in, session_data, must_create) def delete(self, session_key=None): if session_key is None: if self.session_key is None: return session_key = self.session_key backend.delete(session_key)
Es ist wichtig zu verstehen, wie diese Klasse funktioniert, da wir etwas Ähnliches auf Flask implementieren müssen, um Sitzungsdaten zu laden. Schauen wir uns das mit einem REPL-Beispiel genauer an:
>>> from django.conf import settings >>> from django.utils.importlib import import_module >>> engine = import_module(settings.SESSION_ENGINE) >>> engine.SessionStore() <redis_sessions_fork.session.SessionStore object at 0x3761cd0> >>> store["count"] = 1 >>> store.save() >>> store.load() {u'count': 1}
Die Benutzeroberfläche des Sitzungsspeichers ist ziemlich einfach zu verstehen, aber unter der Haube passiert eine Menge. Wir sollten etwas tiefer graben, damit wir etwas Ähnliches auf Flask implementieren können.

Hinweis: Sie könnten fragen: „Warum kopieren Sie nicht einfach die SessionEngine in Flask?“ Leichter gesagt als getan. Wie wir eingangs besprochen haben, ist Django eng mit seinem Settings-Objekt gekoppelt, sodass Sie nicht einfach ein Django-Modul importieren und ohne zusätzliche Arbeit verwenden können.
(De-)Serialisierung von Django-Sitzungen
Wie ich bereits sagte, leistet Django viel Arbeit, um die Komplexität seiner Sitzungsspeicherung zu maskieren. Lassen Sie uns den Redis-Schlüssel überprüfen, der in den obigen Ausschnitten gespeichert ist:
>>> store.session_key u"ery3j462ezmmgebbpwjajlxjxmvt5adu"
Lassen Sie uns nun diesen Schlüssel auf dem redis-cli abfragen:
redis 127.0.0.1:6379> get "django_sessions:ery3j462ezmmgebbpwjajlxjxmvt5adu" "ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ=="
Was wir hier sehen, ist eine sehr lange, Base64-codierte Zeichenfolge. Um ihren Zweck zu verstehen, müssen wir uns die SessionBase
-Klasse von Django ansehen, um zu sehen, wie sie gehandhabt wird:
class SessionBase(object): """ Base class for all Session classes. """ def encode(self, session_dict): "Returns the given session dictionary serialized and encoded as a string." serialized = self.serializer().dumps(session_dict) hash = self._hash(serialized) return base64.b64encode(hash.encode() + b":" + serialized).decode('ascii') def decode(self, session_data): encoded_data = base64.b64decode(force_bytes(session_data)) try: hash, serialized = encoded_data.split(b':', 1) expected_hash = self._hash(serialized) if not constant_time_compare(hash.decode(), expected_hash): raise SuspiciousSession("Session data corrupted") else: return self.serializer().loads(serialized) except Exception as e: # ValueError, SuspiciousOperation, unpickling exceptions if isinstance(e, SuspiciousOperation): logger = logging.getLogger('django.security.%s' % e.__class__.__name__) logger.warning(force_text(e)) return {}
Die encode-Methode serialisiert die Daten zunächst mit dem aktuell registrierten Serializer. Mit anderen Worten, es konvertiert die Sitzung in einen String, den es später wieder in eine Sitzung umwandeln kann (weitere Informationen finden Sie in der SESSION_SERIALIZER-Dokumentation). Dann hasht es die serialisierten Daten und verwendet diesen Hash später als Signatur, um die Integrität der Sitzungsdaten zu überprüfen. Schließlich gibt es dieses Datenpaar als Base64-codierte Zeichenfolge an den Benutzer zurück.
Übrigens: Vor Version 1.6 verwendete Django standardmäßig Pickle für die Serialisierung von Sitzungsdaten. Aus Sicherheitsgründen ist die Standard-Serialisierungsmethode jetzt django.contrib.sessions.serializers.JSONSerializer
.
Codieren einer Beispielsitzung
Sehen wir uns den Sitzungsverwaltungsprozess in Aktion an. Hier wird unser Sitzungswörterbuch einfach eine Anzahl und eine ganze Zahl sein, aber Sie können sich vorstellen, wie sich dies auf kompliziertere Benutzersitzungen verallgemeinern würde.
>>> store.encode({'count': 1}) u'ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ==' >>> base64.b64decode(encoded) 'fe1964e1d2cf8069d9f1823afd143400b6d3736f:{"count":1}'
Das Ergebnis der Store-Methode (u'ZmUxOTY…==') ist eine verschlüsselte Zeichenfolge, die die serialisierte Benutzersitzung und ihren Hash enthält. Wenn wir es entschlüsseln, erhalten wir tatsächlich sowohl den Hash ('fe1964e…') als auch die Sitzung ( {"count":1}
).
Beachten Sie, dass die Dekodierungsmethode überprüft, ob der Hash für diese Sitzung korrekt ist, und die Integrität der Daten garantiert, wenn wir sie in Flask verwenden. In unserem Fall sind wir nicht allzu besorgt darüber, dass unsere Sitzung auf der Clientseite manipuliert wird, weil:
Wir verwenden keine Cookie-basierten Sitzungen, dh wir senden nicht alle Benutzerdaten an den Client.
Auf Flask benötigen wir einen schreibgeschützten
SessionStore
, der uns mitteilt, ob der angegebene Schlüssel vorhanden ist oder nicht, und die gespeicherten Daten zurückgibt.
Verlängerung zum Kolben
Als Nächstes erstellen wir eine vereinfachte Version der Redis-Sitzungs-Engine (Datenbank) für die Arbeit mit Flask. Wir verwenden den gleichen SessionStore
(oben definiert) als Basisklasse, aber wir müssen einige seiner Funktionen entfernen, z. B. das Prüfen auf fehlerhafte Signaturen oder das Modifizieren von Sitzungen. Wir sind mehr an einem schreibgeschützten SessionStore
, der die von Django gespeicherten Sitzungsdaten lädt. Mal sehen, wie es zusammenkommt:
class SessionStore(object): # The default serializer, for now def __init__(self, conn, session_key, secret, serializer=None): self._conn = conn self.session_key = session_key self._secret = secret self.serializer = serializer or JSONSerializer def load(self): session_data = self._conn.get(self.session_key) if not session_data is None: return self._decode(session_data) else: return {} def exists(self, session_key): return self._conn.exists(session_key) def _decode(self, session_data): """ Decodes the Django session :param session_data: :return: decoded data """ encoded_data = base64.b64decode(force_bytes(session_data)) try: # Could produce ValueError if there is no ':' hash, serialized = encoded_data.split(b':', 1) # In the Django version of that they check for corrupted data # I don't find it useful, so I'm removing it return self.serializer().loads(serialized) except Exception as e: # ValueError, SuspiciousOperation, unpickling exceptions. If any of # these happen, return an empty dictionary (ie, empty session). return {}
Wir brauchen nur die load
, da es sich um eine schreibgeschützte Implementierung des Speichers handelt. Das bedeutet, dass Sie sich nicht direkt von Flask abmelden können. Stattdessen möchten Sie diese Aufgabe möglicherweise an Django umleiten. Denken Sie daran, dass das Ziel hier darin besteht, Sitzungen zwischen diesen beiden Python-Frameworks zu verwalten, um Ihnen mehr Flexibilität zu bieten.
Kolben-Sitzungen
Das Flask-Mikroframework unterstützt Cookie-basierte Sitzungen, was bedeutet, dass alle Sitzungsdaten an den Client gesendet werden, Base64-codiert und kryptografisch signiert. Aber eigentlich sind wir nicht sehr an der Session-Unterstützung von Flask interessiert.
Was wir brauchen, ist, die von Django erstellte Sitzungs-ID abzurufen und sie mit dem Redis-Backend zu vergleichen, damit wir sicher sein können, dass die Anfrage zu einem vorsignierten Benutzer gehört. Zusammenfassend wäre der ideale Prozess (dies stimmt mit dem obigen Diagramm überein):
- Wir holen die Django-Sitzungs-ID aus dem Cookie des Benutzers.
- Wenn die Sitzungs-ID in Redis gefunden wird, geben wir die Sitzung zurück, die dieser ID entspricht.
- Wenn nicht, leiten wir sie auf eine Anmeldeseite um.
Es ist praktisch, einen Dekorateur zu haben, der nach diesen Informationen sucht und die aktuelle user_id
in die g
-Variable in Flask eingibt:
from functools import wraps from flask import g, request, redirect, url_for def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): djsession_id = request.cookies.get("sessionid") if djsession_id is None: return redirect("/") key = get_session_prefixed(djsession_id) session_store = SessionStore(redis_conn, key) auth = session_store.load() if not auth: return redirect("/") g.user_id = str(auth.get("_auth_user_id")) return f(*args, **kwargs) return decorated_function
Im obigen Beispiel verwenden wir immer noch den zuvor definierten SessionStore
, um die Django-Daten aus Redis abzurufen. Wenn die Sitzung eine _auth_user_id
hat, geben wir den Inhalt von der Ansichtsfunktion zurück; Andernfalls wird der Benutzer auf eine Anmeldeseite umgeleitet, genau wie wir es wollten.
Dinge zusammenkleben
Um Cookies zu teilen, finde ich es praktisch, Django und Flask über einen WSGI-Server zu starten und zusammenzukleben. In diesem Beispiel habe ich CherryPy verwendet:
from app import app from django.core.wsgi import get_wsgi_application application = get_wsgi_application() d = wsgiserver.WSGIPathInfoDispatcher({ "/":application, "/backend":app }) server = wsgiserver.CherryPyWSGIServer(("127.0.0.1", 8080), d)
Damit wird Django auf „/“ und Flask auf „/backend“-Endpunkten bereitgestellt.
Abschließend
Anstatt Django im Vergleich zu Flask zu untersuchen oder Sie zu ermutigen, nur das Flask-Mikroframework zu lernen, habe ich Django und Flask zusammengeschweißt und sie dazu gebracht, dieselben Sitzungsdaten zur Authentifizierung zu teilen, indem ich die Aufgabe an Django delegiere. Da Django mit zahlreichen Modulen zur Benutzerregistrierung, Anmeldung und Abmeldung ausgeliefert wird (um nur einige zu nennen), spart Ihnen die Kombination dieser beiden Frameworks wertvolle Zeit und bietet Ihnen gleichzeitig die Möglichkeit, ein überschaubares Mikroframework wie Flask zu hacken.