Tutorial Django, Flask și Redis: Gestionarea sesiunilor de aplicații web între cadrele Python
Publicat: 2022-03-11Django Versus Flask: Când Django este alegerea greșită
Iubesc și folosesc Django în multe dintre proiectele mele personale și ale clientului, mai ales pentru aplicații web mai clasice și cele care implică baze de date relaționale. Cu toate acestea, Django nu este un glonț de argint.
Prin design, Django este foarte strâns cuplat cu ORM, Sistemul de motor șabloane și obiectul Setări. În plus, nu este un proiect nou: poartă o mulțime de bagaje pentru a rămâne compatibil cu retroactiv.
Unii dezvoltatori Python văd aceasta ca o problemă majoră. Ei spun că Django nu este suficient de flexibil și evită-l dacă este posibil și, în schimb, folosește un microframework Python precum Flask.
Nu împărtășesc această părere. Django este grozav atunci când este utilizat în locul și timpul potrivit, chiar dacă nu se încadrează în fiecare specificație de proiect. După cum spune mantra: „Folosește instrumentul potrivit pentru muncă”.
(Chiar și atunci când nu este locul și momentul potrivit, uneori programarea cu Django poate avea beneficii unice.)
În unele cazuri, poate fi într-adevăr frumos să folosiți un cadru mai ușor (cum ar fi Flask). Adesea, aceste microcadre încep să strălucească atunci când îți dai seama cât de ușor sunt de piratat.
Microframeworks to the Rescue
În câteva dintre proiectele mele client, am discutat despre renunțarea la Django și trecerea la un microframework, de obicei atunci când clienții doresc să facă niște lucruri interesante (într-un caz, de exemplu, încorporarea ZeroMQ în obiectul aplicației) și despre proiect obiectivele par mai greu de atins cu Django.
În general, consider că Flask este util pentru:
- Backend-uri simple REST API
- Aplicații care nu necesită acces la baza de date
- Aplicații web bazate pe NoSQL
- Aplicații web cu cerințe foarte specifice, cum ar fi configurații URL personalizate
În același timp, aplicația noastră necesita înregistrarea utilizatorilor și alte sarcini comune pe care Django le-a rezolvat cu ani în urmă. Având în vedere greutatea sa redusă, Flask nu vine cu același set de instrumente.
A apărut întrebarea: este Django o afacere cu totul sau nimic? Ar trebui să-l renunțăm complet din proiect sau putem învăța să-l combinăm cu flexibilitatea altor microframework-uri sau cadre tradiționale? Putem alege și alege piesele pe care vrem să le folosim și să evitam altele?
Putem avea ce este mai bun din ambele lumi? Spun da, mai ales când vine vorba de managementul sesiunilor.
(Ca să nu mai vorbim că există o mulțime de proiecte pentru freelanceri Django.)
Acum, Tutorialul Python: Partajarea sesiunilor Django
Scopul acestei postări este de a delega sarcinile de autentificare și înregistrare a utilizatorilor către Django, dar folosiți Redis pentru a partaja sesiunile utilizatorilor cu alte cadre. Mă pot gândi la câteva scenarii în care ceva de genul acesta ar fi util:
- Trebuie să dezvoltați un API REST separat de aplicația Django, dar doriți să partajați datele sesiunii.
- Aveți o componentă specifică care poate fi nevoie să fie înlocuită mai târziu sau extinsă dintr-un anumit motiv și încă mai aveți nevoie de date de sesiune.
Pentru acest tutorial, voi folosi Redis pentru a partaja sesiuni între două cadre (în acest caz, Django și Flask). În configurația actuală, voi folosi SQLite pentru a stoca informații despre utilizator, dar puteți avea back-end-ul legat de o bază de date NoSQL (sau o alternativă bazată pe SQL), dacă este necesar.
Sesiuni de înțelegere
Pentru a partaja sesiuni între Django și Flask, trebuie să știm puțin despre modul în care Django își stochează informațiile despre sesiune. Documentele Django sunt destul de bune, dar voi oferi câteva informații de bază pentru a fi complet.
Soiuri de management al sesiunii
În general, puteți alege să gestionați datele de sesiune ale aplicației dvs. Python într-unul din două moduri:
Sesiuni bazate pe cookie-uri : în acest scenariu, datele sesiunii nu sunt stocate într-un depozit de date din back-end. În schimb, este serializat, semnat (cu SECRET_KEY) și trimis clientului. Când clientul trimite acele date înapoi, integritatea lor este verificată pentru manipulare și este deserializat din nou pe server.
Sesiuni bazate pe stocare : În acest scenariu, datele sesiunii în sine nu sunt trimise clientului. În schimb, este trimisă doar o mică parte (o cheie) pentru a indica identitatea utilizatorului curent, stocată în magazinul de sesiuni.
În exemplul nostru, suntem mai interesați de ultimul scenariu: dorim ca datele noastre de sesiune să fie stocate pe back-end și apoi verificate în Flask. Același lucru s-ar putea face și în primul, dar așa cum menționează documentația Django, există unele preocupări cu privire la securitatea primei metode.
Fluxul de lucru general
Fluxul de lucru general al gestionării și gestionării sesiunilor va fi similar cu această diagramă:
Să trecem prin partajarea sesiunii mai detaliat:
Când apare o nouă solicitare, primul pas este să o trimiteți prin middleware-ul înregistrat în stiva Django. Suntem interesați aici de clasa
SessionMiddleware
care, așa cum v-ați putea aștepta, este legată de gestionarea și gestionarea sesiunilor: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)
În acest fragment, Django
SessionEngine
înregistrat (vom ajunge la asta în curând), extrageSESSION_COOKIE_NAME
dinrequest
(sessionid
, în mod implicit) și creează o nouă instanță aSessionEngine
selectat pentru a gestiona stocarea sesiunii.
Mai târziu (după ce vizualizarea utilizatorului este procesată, dar încă în stiva de middleware), motorul de sesiune își apelează metoda de salvare pentru a salva orice modificări aduse depozitului de date. (În timpul manipulării vizualizării, este posibil ca utilizatorul să fi schimbat câteva lucruri în cadrul sesiunii, de exemplu, prin adăugarea unei noi valori la obiectul sesiune cu
request.session
.) Apoi,SESSION_COOKIE_NAME
este trimis clientului. Iată versiunea simplificată: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
Ne interesează în special clasa SessionEngine
, pe care o vom înlocui cu ceva care să stocheze și să încarce date către și de la un back-end Redis.
Din fericire, există câteva proiecte care se ocupă deja de acest lucru pentru noi. Iată un exemplu de la redis_sessions_fork . Acordați o atenție deosebită metodelor de save
și load
, care sunt scrise astfel încât (respectiv) să stocheze și să încarce sesiunea în și din Redis:
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)
Este important să înțelegem cum funcționează această clasă, deoarece va trebui să implementăm ceva similar pe Flask pentru a încărca datele sesiunii. Să aruncăm o privire mai atentă cu un exemplu REPL:
>>> 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}
Interfața magazinului de sesiuni este destul de ușor de înțeles, dar se întâmplă multe sub capotă. Ar trebui să săpăm puțin mai adânc, astfel încât să putem implementa ceva similar pe Flask.

Notă: ați putea întreba: „De ce nu copiați SessionEngine în Flask?” Mai ușor de zis decât de făcut. După cum am discutat la început, Django este strâns cuplat cu obiectul Setări, așa că nu puteți pur și simplu să importați un modul Django și să-l utilizați fără nicio muncă suplimentară.
(De-)Serializarea sesiunii Django
După cum am spus, Django lucrează mult pentru a masca complexitatea stocării sesiunii sale. Să verificăm cheia Redis care este stocată în fragmentele de mai sus:
>>> store.session_key u"ery3j462ezmmgebbpwjajlxjxmvt5adu"
Acum, să interogăm acea cheie pe redis-cli:
redis 127.0.0.1:6379> get "django_sessions:ery3j462ezmmgebbpwjajlxjxmvt5adu" "ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ=="
Ceea ce vedem aici este un șir foarte lung, codificat în Base64. Pentru a-i înțelege scopul, trebuie să ne uităm la clasa SessionBase
a Django pentru a vedea cum este gestionată:
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 {}
Metoda de codificare serializează mai întâi datele cu serializatorul înregistrat curent. Cu alte cuvinte, convertește sesiunea într-un șir, pe care ulterior îl poate converti înapoi într-o sesiune (consultați documentația SESSION_SERIALIZER pentru mai multe). Apoi, indexează datele serializate și folosește acest hash ulterior ca semnătură pentru a verifica integritatea datelor sesiunii. În cele din urmă, returnează acea pereche de date utilizatorului ca șir codificat în Base64.
Apropo: înainte de versiunea 1.6, Django folosea implicit pickle pentru serializarea datelor de sesiune. Din cauza problemelor de securitate, metoda implicită de serializare este acum django.contrib.sessions.serializers.JSONSerializer
.
Codificarea unei sesiuni de exemplu
Să vedem procesul de gestionare a sesiunii în acțiune. Aici, dicționarul nostru de sesiune va fi pur și simplu un număr și un număr întreg, dar vă puteți imagina cum s-ar generaliza acest lucru la sesiuni de utilizator mai complicate.
>>> store.encode({'count': 1}) u'ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ==' >>> base64.b64decode(encoded) 'fe1964e1d2cf8069d9f1823afd143400b6d3736f:{"count":1}'
Rezultatul metodei de stocare (u'ZmUxOTY…==') este un șir codificat care conține sesiunea de utilizator serializată și hash-ul acesteia. Când îl decodăm, primim într-adevăr atât hash-ul ('fe1964e...') cât și sesiunea ( {"count":1}
).
Rețineți că metoda de decodare verifică pentru a se asigura că hash-ul este corect pentru acea sesiune, garantând integritatea datelor atunci când mergem să le folosim în Flask. În cazul nostru, nu suntem prea îngrijorați de modificarea sesiunii noastre din partea clientului, deoarece:
Nu folosim sesiuni bazate pe cookie-uri, adică nu trimitem toate datele utilizatorului către client.
Pe Flask, vom avea nevoie de un
SessionStore
doar pentru citire, care ne va spune dacă există sau nu cheia dată și va returna datele stocate.
Se extinde la Flask
Apoi, să creăm o versiune simplificată a motorului de sesiune Redis (bază de date) pentru a lucra cu Flask. Vom folosi același SessionStore
(definit mai sus) ca clasă de bază, dar va trebui să eliminăm o parte din funcționalitatea acestuia, de exemplu, verificarea semnăturilor greșite sau modificarea sesiunilor. Suntem mai interesați de un SessionStore
doar pentru citire, care va încărca datele sesiunii salvate din Django. Să vedem cum se îmbină:
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 {}
Avem nevoie doar de metoda de load
, deoarece este o implementare doar pentru citire a stocării. Aceasta înseamnă că nu vă puteți deconecta direct de la Flask; în schimb, ați putea dori să redirecționați această sarcină către Django. Amintiți-vă, scopul aici este de a gestiona sesiunile dintre aceste două cadre Python pentru a vă oferi mai multă flexibilitate.
Sesiuni cu balon
Microframework-ul Flask acceptă sesiuni bazate pe cookie-uri, ceea ce înseamnă că toate datele sesiunii sunt trimise către client, codificate în Base64 și semnate criptografic. Dar, de fapt, nu suntem foarte interesați de suportul pentru sesiuni de la Flask.
Ceea ce avem nevoie este să obținem ID-ul de sesiune creat de Django și să îl verificăm cu back-end-ul Redis, astfel încât să putem fi siguri că cererea aparține unui utilizator presemnat. În rezumat, procesul ideal ar fi (acest lucru se sincronizează cu diagrama de mai sus):
- Luăm ID-ul sesiunii Django din cookie-ul utilizatorului.
- Dacă ID-ul sesiunii este găsit în Redis, returnăm sesiunea care se potrivește cu acel ID.
- Dacă nu, îi redirecționăm către o pagină de conectare.
Va fi util să aveți un decorator care să verifice acele informații și să seteze user_id
-ul curent în variabila g
din Flask:
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
În exemplul de mai sus, încă folosim SessionStore
pe care l-am definit anterior pentru a prelua datele Django de la Redis. Dacă sesiunea are un _auth_user_id
, returnăm conținutul din funcția de vizualizare; în caz contrar, utilizatorul este redirecționat către o pagină de autentificare, așa cum ne-am dorit.
Lipirea lucrurilor împreună
Pentru a partaja cookie-uri, mi se pare convenabil să porniți Django și Flask printr-un server WSGI și să le lipiți împreună. În acest exemplu, am folosit CherryPy:
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)
Cu asta, Django va servi pe „/”, iar Flask va servi pe punctele finale „/backend”.
În concluzie
În loc să examinez Django versus Flask sau să vă încurajez doar să învățați microcadrul Flask, am sudat împreună Django și Flask, făcându-le să partajeze aceleași date de sesiune pentru autentificare prin delegarea sarcinii lui Django. Deoarece Django este livrat cu o mulțime de module pentru a rezolva înregistrarea utilizatorilor, autentificarea și deconectarea (pentru a numi doar câteva), combinarea acestor două cadre vă va economisi timp prețios, oferindu-vă în același timp oportunitatea de a pirata un microcadru gestionabil precum Flask.