Esercitazione su Django, Flask e Redis: gestione delle sessioni di applicazioni Web tra framework Python
Pubblicato: 2022-03-11Django contro Flask: Quando Django è la scelta sbagliata
Amo e uso Django in molti dei miei progetti personali e dei clienti, principalmente per applicazioni web più classiche e quelle che coinvolgono database relazionali. Tuttavia, Django non è un proiettile d'argento.
In base alla progettazione, Django è strettamente associato al suo oggetto ORM, Template Engine System e Settings. Inoltre, non è un progetto nuovo: porta molto bagaglio per rimanere retrocompatibile.
Alcuni sviluppatori Python vedono questo come un grosso problema. Dicono che Django non sia abbastanza flessibile e lo evitano se possibile e, invece, usano un microframework Python come Flask.
Non condivido questa opinione. Django è ottimo se utilizzato nel luogo e nel tempo appropriati, anche se non si adatta a tutte le specifiche del progetto. Come dice il mantra: "Usa lo strumento giusto per il lavoro".
(Anche quando non è il posto e l'ora giusti, a volte la programmazione con Django può avere vantaggi unici.)
In alcuni casi, può essere davvero utile utilizzare un framework più leggero (come Flask). Spesso, questi microframework iniziano a brillare quando ti rendi conto di quanto siano facili da hackerare.
Microquadri in soccorso
In alcuni dei miei progetti client, abbiamo discusso di rinunciare a Django e passare a un microframework, in genere quando i clienti vogliono fare cose interessanti (in un caso, ad esempio, incorporare ZeroMQ nell'oggetto dell'applicazione) e il progetto gli obiettivi sembrano più difficili da raggiungere con Django.
Più in generale, trovo Flask utile per:
- Backend API REST semplici
- Applicazioni che non richiedono l'accesso al database
- App Web basate su NoSQL
- App Web con requisiti molto specifici, come configurazioni URL personalizzate
Allo stesso tempo, la nostra app richiedeva la registrazione dell'utente e altre attività comuni che Django ha risolto anni fa. Dato il suo peso leggero, Flask non viene fornito con lo stesso toolkit.
È emersa la domanda: Django è un affare tutto o niente? Dovremmo eliminarlo completamente dal progetto o possiamo imparare a combinarlo con la flessibilità di altri microframework o framework tradizionali? Possiamo scegliere i pezzi che vogliamo usare ed evitare gli altri?
Possiamo avere il meglio di entrambi i mondi? Dico di sì, soprattutto quando si tratta di gestione delle sessioni.
(Per non parlare del fatto che ci sono molti progetti là fuori per i freelance di Django.)
Ora il tutorial Python: condivisione di sessioni di Django
L'obiettivo di questo post è delegare le attività di autenticazione e registrazione dell'utente a Django, ma utilizzare Redis per condividere sessioni utente con altri framework. Posso pensare ad alcuni scenari in cui qualcosa del genere potrebbe essere utile:
- Devi sviluppare un'API REST separatamente dalla tua app Django ma vuoi condividere i dati della sessione.
- Hai un componente specifico che potrebbe dover essere sostituito in un secondo momento o ridimensionato per qualche motivo e hai ancora bisogno dei dati della sessione.
Per questo tutorial, userò Redis per condividere sessioni tra due framework (in questo caso, Django e Flask). Nella configurazione corrente, userò SQLite per archiviare le informazioni sull'utente, ma puoi avere il tuo back-end legato a un database NoSQL (o un'alternativa basata su SQL), se necessario.
Comprendere le sessioni
Per condividere sessioni tra Django e Flask, abbiamo bisogno di sapere un po' come Django memorizza le informazioni sulla sessione. I documenti di Django sono abbastanza buoni, ma fornirò alcune informazioni di base per completezza.
Varietà di gestione delle sessioni
In genere, puoi scegliere di gestire i dati della sessione dell'app Python in uno dei due modi seguenti:
Sessioni basate sui cookie : in questo scenario, i dati della sessione non vengono archiviati in un archivio dati sul back-end. Viene invece serializzato, firmato (con una SECRET_KEY) e inviato al client. Quando il client restituisce quei dati, la sua integrità viene verificata per la manomissione e viene nuovamente deserializzato sul server.
Sessioni basate sull'archiviazione : in questo scenario, i dati della sessione stessa non vengono inviati al client. Al contrario, viene inviata solo una piccola parte (una chiave) per indicare l'identità dell'utente corrente, archiviata nell'archivio sessioni.
Nel nostro esempio, siamo più interessati al secondo scenario: vogliamo che i nostri dati di sessione siano archiviati nel back-end e quindi controllati in Flask. La stessa cosa potrebbe essere fatta nel primo, ma come menziona la documentazione di Django, ci sono alcune preoccupazioni sulla sicurezza del primo metodo.
Il flusso di lavoro generale
Il flusso di lavoro generale di gestione e gestione delle sessioni sarà simile a questo diagramma:
Esaminiamo la condivisione della sessione in modo un po' più dettagliato:
Quando arriva una nuova richiesta, il primo passo è inviarla tramite il middleware registrato nello stack di Django. Siamo interessati qui alla classe
SessionMiddleware
che, come ci si potrebbe aspettare, è correlata alla gestione e alla gestione delle sessioni: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 questo frammento di codice, Django acquisisce il
SessionEngine
registrato (ci arriveremo presto), estraeSESSION_COOKIE_NAME
dallarequest
(sessionid
, per impostazione predefinita) e crea una nuova istanza delSessionEngine
selezionato per gestire l'archiviazione della sessione.
Successivamente (dopo che la vista utente è stata elaborata, ma ancora nello stack del middleware), il motore di sessione chiama il suo metodo di salvataggio per salvare eventuali modifiche nell'archivio dati. (Durante la gestione della visualizzazione, l'utente potrebbe aver modificato alcune cose all'interno della sessione, ad esempio aggiungendo un nuovo valore all'oggetto sessione con
request.session
.) Quindi,SESSION_COOKIE_NAME
viene inviato al client. Ecco la versione semplificata: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
Siamo particolarmente interessati alla classe SessionEngine
, che sostituiremo con qualcosa per archiviare e caricare dati da e verso un back-end Redis.
Fortunatamente, ci sono alcuni progetti che già gestiscono questo per noi. Ecco un esempio da redis_sessions_fork . Presta molta attenzione ai metodi di save
e load
, che sono scritti in modo da (rispettivamente) archiviare e caricare la sessione in e da 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)
È importante capire come funziona questa classe poiché dovremo implementare qualcosa di simile su Flask per caricare i dati della sessione. Diamo un'occhiata più da vicino con un esempio 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}
L'interfaccia del negozio di sessioni è abbastanza facile da capire, ma c'è molto da fare sotto il cofano. Dovremmo scavare un po' più a fondo in modo da poter implementare qualcosa di simile su Flask.

Nota: potresti chiedere: "Perché non copiare semplicemente SessionEngine in Flask?" Più facile a dirsi che a farsi. Come abbiamo discusso all'inizio, Django è strettamente accoppiato con il suo oggetto Settings, quindi non puoi semplicemente importare alcuni moduli Django e usarlo senza alcun lavoro aggiuntivo.
Django Session (De-)serializzazione
Come ho detto, Django fa molto lavoro per mascherare la complessità della sua memorizzazione delle sessioni. Controlliamo la chiave Redis memorizzata negli snippet sopra:
>>> store.session_key u"ery3j462ezmmgebbpwjajlxjxmvt5adu"
Ora, interroghiamo quella chiave su redis-cli:
redis 127.0.0.1:6379> get "django_sessions:ery3j462ezmmgebbpwjajlxjxmvt5adu" "ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ=="
Quello che vediamo qui è una stringa codificata in Base64 molto lunga. Per capire il suo scopo, dobbiamo guardare la classe SessionBase
di Django per vedere come viene gestita:
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 {}
Il metodo encode prima serializza i dati con il serializzatore registrato corrente. In altre parole, converte la sessione in una stringa, che può poi riconvertire in una sessione (guarda la documentazione SESSION_SERIALIZER per ulteriori informazioni). Quindi, esegue l'hashing dei dati serializzati e utilizza questo hash in seguito come firma per verificare l'integrità dei dati della sessione. Infine, restituisce quella coppia di dati all'utente come stringa con codifica Base64.
A proposito: prima della versione 1.6, Django utilizzava per impostazione predefinita pickle per la serializzazione dei dati della sessione. Per motivi di sicurezza, il metodo di serializzazione predefinito è ora django.contrib.sessions.serializers.JSONSerializer
.
Codificare una sessione di esempio
Vediamo in azione il processo di gestione della sessione. Qui, il nostro dizionario di sessione sarà semplicemente un conteggio e un numero intero, ma puoi immaginare come questo si generalizzerebbe a sessioni utente più complicate.
>>> store.encode({'count': 1}) u'ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ==' >>> base64.b64decode(encoded) 'fe1964e1d2cf8069d9f1823afd143400b6d3736f:{"count":1}'
Il risultato del metodo store (u'ZmUxOTY…==') è una stringa codificata contenente la sessione utente serializzata e il relativo hash. Quando lo decodifichiamo, otteniamo effettivamente sia l'hash ('fe1964e…') che la sessione ( {"count":1}
).
Si noti che il metodo di decodifica verifica che l'hash sia corretto per quella sessione, garantendo l'integrità dei dati quando andiamo a utilizzarli in Flask. Nel nostro caso, non siamo troppo preoccupati che la nostra sessione venga manomessa dal lato client perché:
Non stiamo utilizzando sessioni basate su cookie, ovvero non stiamo inviando tutti i dati dell'utente al client.
Su Flask, avremo bisogno di un
SessionStore
di sola lettura che ci dirà se la chiave fornita esiste o meno e restituirà i dati memorizzati.
Estendendosi a Flask
Quindi, creiamo una versione semplificata del motore di sessione Redis (database) per funzionare con Flask. Utilizzeremo lo stesso SessionStore
(definito sopra) come classe base, ma dovremo rimuovere alcune delle sue funzionalità, ad esempio, il controllo di firme errate o la modifica di sessioni. Siamo più interessati a un SessionStore
di sola lettura che caricherà i dati della sessione salvati da Django. Vediamo come si combina:
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 {}
Abbiamo solo bisogno del metodo di load
perché è un'implementazione di sola lettura dello storage. Ciò significa che non puoi disconnetterti direttamente da Flask; invece, potresti voler reindirizzare questa attività a Django. Ricorda, l'obiettivo qui è gestire le sessioni tra questi due framework Python per darti maggiore flessibilità.
Sessioni di boccette
Il microframework Flask supporta sessioni basate su cookie, il che significa che tutti i dati della sessione vengono inviati al client, codificati in Base64 e firmati crittograficamente. Ma in realtà non siamo molto interessati al supporto per le sessioni di Flask.
Quello di cui abbiamo bisogno è ottenere l'ID di sessione creato da Django e confrontarlo con il back-end Redis in modo da poter essere sicuri che la richiesta appartenga a un utente pre-firmato. In sintesi, il processo ideale sarebbe (questo si sincronizza con il diagramma sopra):
- Prendiamo l'ID della sessione Django dal cookie dell'utente.
- Se l'ID della sessione viene trovato in Redis, restituiamo la sessione corrispondente a quell'ID.
- In caso contrario, li reindirizziamo a una pagina di accesso.
Sarà utile avere un decoratore per controllare tali informazioni e impostare l'attuale user_id
nella variabile g
in 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
Nell'esempio sopra, stiamo ancora utilizzando il SessionStore
che abbiamo definito in precedenza per recuperare i dati di Django da Redis. Se la sessione ha un _auth_user_id
, restituiamo il contenuto dalla funzione di visualizzazione; in caso contrario, l'utente viene reindirizzato a una pagina di accesso, proprio come volevamo.
Incollare le cose insieme
Per condividere i cookie, trovo conveniente avviare Django e Flask tramite un server WSGI e incollarli insieme. In questo esempio, ho usato 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)
Con ciò, Django servirà su "/" e Flask servirà su endpoint "/ backend".
In conclusione
Invece di esaminare Django contro Flask o incoraggiarti solo ad apprendere il microframework Flask, ho unito Django e Flask, facendoli condividere gli stessi dati di sessione per l'autenticazione delegando l'attività a Django. Poiché Django viene fornito con molti moduli per risolvere la registrazione, l'accesso e il logout degli utenti (solo per citarne alcuni), la combinazione di questi due framework ti farà risparmiare tempo prezioso fornendoti l'opportunità di hackerare un microframework gestibile come Flask.