Tutoriel Django, Flask et Redis : gestion des sessions d'application Web entre les frameworks Python

Publié: 2022-03-11

Django Versus Flask : Quand Django est le mauvais choix

J'adore et j'utilise Django dans de nombreux projets personnels et clients, principalement pour des applications Web plus classiques et celles impliquant des bases de données relationnelles. Cependant, Django n'est pas une solution miracle.

De par sa conception, Django est très étroitement couplé à son objet ORM, Template Engine System et Settings. De plus, ce n'est pas un nouveau projet : il porte beaucoup de bagages pour rester rétrocompatible.

Certains développeurs Python voient cela comme un problème majeur. Ils disent que Django n'est pas assez flexible et l'évitent si possible et utilisent plutôt un microframework Python comme Flask.

Je ne partage pas cet avis. Django est génial lorsqu'il est utilisé au bon endroit et au bon moment, même s'il ne rentre pas dans toutes les spécifications de projet. Comme le dit le mantra : "Utilisez le bon outil pour le travail".

(Même lorsque ce n'est pas le bon endroit et le bon moment, parfois programmer avec Django peut avoir des avantages uniques.)

Dans certains cas, il peut en effet être agréable d'utiliser un framework plus léger (comme Flask). Souvent, ces microframeworks commencent à briller lorsque vous réalisez à quel point ils sont faciles à pirater.

Les microframeworks à la rescousse

Dans quelques-uns de mes projets clients, nous avons discuté d'abandonner Django et de passer à un microframework, généralement lorsque les clients veulent faire des choses intéressantes (dans un cas, par exemple, intégrer ZeroMQ dans l'objet d'application) et le projet les objectifs semblent plus difficiles à atteindre avec Django.

Plus généralement, je trouve Flask utile pour :

  • Backends d'API REST simples
  • Applications qui ne nécessitent pas d'accès à la base de données
  • Applications Web basées sur NoSQL
  • Applications Web avec des exigences très spécifiques, telles que des configurations d'URL personnalisées

Dans le même temps, notre application nécessitait l'enregistrement des utilisateurs et d'autres tâches courantes que Django avait résolues il y a des années. Compte tenu de son poids léger, Flask n'est pas livré avec la même boîte à outils.

La question a émergé : Django est-il un accord tout ou rien ?

La question a émergé : Django est-il un accord tout ou rien ? Doit-on l'abandonner complètement du projet, ou pouvons-nous apprendre à le combiner avec la flexibilité d'autres microframeworks ou frameworks traditionnels ? Pouvons-nous choisir les pièces que nous voulons utiliser et éviter les autres ?

Pouvons-nous avoir le meilleur des deux mondes ? Je dis oui, surtout en ce qui concerne la gestion des sessions.

(Sans oublier qu'il existe de nombreux projets pour les indépendants de Django.)

Maintenant le tutoriel Python : Partage de sessions Django

L'objectif de cet article est de déléguer les tâches d'authentification et d'enregistrement des utilisateurs à Django, tout en utilisant Redis pour partager des sessions utilisateur avec d'autres frameworks. Je peux penser à quelques scénarios dans lesquels quelque chose comme ceci serait utile:

  • Vous devez développer une API REST séparément de votre application Django mais souhaitez partager les données de session.
  • Vous avez un composant spécifique qui devra peut-être être remplacé ultérieurement ou mis à l'échelle pour une raison quelconque et qui a toujours besoin de données de session.

Pour ce tutoriel, j'utiliserai Redis pour partager des sessions entre deux frameworks (dans ce cas, Django et Flask). Dans la configuration actuelle, j'utiliserai SQLite pour stocker les informations utilisateur, mais vous pouvez avoir votre back-end lié à une base de données NoSQL (ou une alternative basée sur SQL) si nécessaire.

Comprendre les séances

Pour partager des sessions entre Django et Flask, nous devons en savoir un peu plus sur la façon dont Django stocke ses informations de session. Les documents Django sont plutôt bons, mais je vais fournir quelques informations de base pour être complet.

Variétés de gestion de session

Généralement, vous pouvez choisir de gérer les données de session de votre application Python de deux manières :

  • Sessions basées sur les cookies : dans ce scénario, les données de session ne sont pas stockées dans un magasin de données sur le back-end. Au lieu de cela, il est sérialisé, signé (avec une SECRET_KEY) et envoyé au client. Lorsque le client renvoie ces données, leur intégrité est vérifiée pour détecter toute falsification et elles sont à nouveau désérialisées sur le serveur.

  • Sessions basées sur le stockage : dans ce scénario, les données de session elles-mêmes ne sont pas envoyées au client. Au lieu de cela, seule une petite partie est envoyée (une clé) pour indiquer l'identité de l'utilisateur actuel, stockée dans le magasin de session.

Dans notre exemple, nous sommes plus intéressés par ce dernier scénario : nous voulons que nos données de session soient stockées sur le back-end, puis vérifiées dans Flask. La même chose pourrait être faite dans le premier cas, mais comme le mentionne la documentation de Django, la sécurité de la première méthode suscite des inquiétudes.

Le flux de travail général

Le flux de travail général de la gestion et de la gestion des sessions sera similaire à ce diagramme :

Un schéma montrant la gestion des sessions utilisateur entre Flask et Django à l'aide de Redis.

Passons en revue le partage de session un peu plus en détail :

  1. Lorsqu'une nouvelle requête arrive, la première étape consiste à l'envoyer via le middleware enregistré dans la pile Django. Nous nous intéressons ici à la classe SessionMiddleware qui, comme vous vous en doutez, est liée à la gestion et à la gestion des sessions :

     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)

    Dans cet extrait, Django récupère le SessionEngine enregistré (nous y reviendrons bientôt), extrait le SESSION_COOKIE_NAME de la request ( sessionid , par défaut) et crée une nouvelle instance du SessionEngine sélectionné pour gérer le stockage de session.

  • Plus tard (après le traitement de la vue utilisateur, mais toujours dans la pile middleware), le moteur de session appelle sa méthode save pour enregistrer toutes les modifications apportées au magasin de données. (Pendant la gestion de la vue, l'utilisateur peut avoir changé quelques éléments dans la session, par exemple en ajoutant une nouvelle valeur à l'objet de session avec request.session .) Ensuite, le SESSION_COOKIE_NAME est envoyé au client. Voici la version simplifiée :

     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

Nous sommes particulièrement intéressés par la classe SessionEngine , que nous remplacerons par quelque chose pour stocker et charger des données vers et depuis un back-end Redis.

Heureusement, il y a quelques projets qui gèrent déjà cela pour nous. Voici un exemple de redis_sessions_fork . Portez une attention particulière aux méthodes de save et load , qui sont écrites de manière à (respectivement) stocker et charger la session dans et depuis 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)

Il est important de comprendre comment cette classe fonctionne car nous devrons implémenter quelque chose de similaire sur Flask pour charger les données de session. Regardons de plus près avec un exemple 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'interface du magasin de session est assez facile à comprendre, mais il se passe beaucoup de choses sous le capot. Nous devrions creuser un peu plus pour pouvoir implémenter quelque chose de similaire sur Flask.

Remarque : Vous pourriez demander : " Pourquoi ne pas simplement copier le SessionEngine dans Flask ?" Plus facile à dire qu'à faire. Comme nous en avons discuté au début, Django est étroitement lié à son objet Settings, vous ne pouvez donc pas simplement importer un module Django et l'utiliser sans aucun travail supplémentaire.

Django Session (Dé-)Sérialisation

Comme je l'ai dit, Django fait beaucoup de travail pour masquer la complexité de son stockage de session. Vérifions la clé Redis qui est stockée dans les extraits ci-dessus :

 >>> store.session_key u"ery3j462ezmmgebbpwjajlxjxmvt5adu"

Maintenant, interrogeons cette clé sur le redis-cli :

 redis 127.0.0.1:6379> get "django_sessions:ery3j462ezmmgebbpwjajlxjxmvt5adu" "ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ=="

Ce que nous voyons ici est une très longue chaîne encodée en Base64. Pour comprendre son objectif, nous devons examiner la classe SessionBase de Django pour voir comment elle est gérée :

 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 {}

La méthode d'encodage sérialise d'abord les données avec le sérialiseur actuellement enregistré. En d'autres termes, il convertit la session en une chaîne, qu'il peut ensuite reconvertir en session (consultez la documentation SESSION_SERIALIZER pour en savoir plus). Ensuite, il hache les données sérialisées et utilise ce hachage plus tard comme signature pour vérifier l'intégrité des données de session. Enfin, il renvoie cette paire de données à l'utilisateur sous la forme d'une chaîne encodée en Base64.

Au fait : avant la version 1.6, Django utilisait par défaut pickle pour la sérialisation des données de session. Pour des raisons de sécurité, la méthode de sérialisation par défaut est désormais django.contrib.sessions.serializers.JSONSerializer .

Encodage d'un exemple de session

Voyons le processus de gestion de session en action. Ici, notre dictionnaire de session sera simplement un nombre et un nombre entier, mais vous pouvez imaginer comment cela se généraliserait à des sessions utilisateur plus compliquées.

 >>> store.encode({'count': 1}) u'ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ==' >>> base64.b64decode(encoded) 'fe1964e1d2cf8069d9f1823afd143400b6d3736f:{"count":1}'

Le résultat de la méthode store (u'ZmUxOTY…==') est une chaîne encodée contenant la session utilisateur sérialisée et son hachage. Lorsque nous le décodons, nous récupérons en effet à la fois le hash ('fe1964e…') et la session ( {"count":1} ).

Notez que la méthode de décodage vérifie que le hachage est correct pour cette session, garantissant l'intégrité des données lorsque nous allons les utiliser dans Flask. Dans notre cas, nous ne craignons pas trop que notre session soit falsifiée côté client car :

  • Nous n'utilisons pas de sessions basées sur des cookies, c'est-à-dire que nous n'envoyons pas toutes les données de l'utilisateur au client.

  • Sur Flask, nous aurons besoin d'un SessionStore en lecture seule qui nous indiquera si la clé donnée existe ou non et renverra les données stockées.

Extension au flacon

Ensuite, créons une version simplifiée du moteur de session Redis (base de données) pour travailler avec Flask. Nous utiliserons le même SessionStore (défini ci-dessus) comme classe de base, mais nous devrons supprimer certaines de ses fonctionnalités, par exemple, vérifier les mauvaises signatures ou modifier les sessions. Nous sommes plus intéressés par un SessionStore en lecture seule qui chargera les données de session enregistrées à partir de Django. Voyons comment ça s'articule :

 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 {}

Nous n'avons besoin que de la méthode load car il s'agit d'une implémentation en lecture seule du stockage. Cela signifie que vous ne pouvez pas vous déconnecter directement de Flask ; à la place, vous voudrez peut-être rediriger cette tâche vers Django. N'oubliez pas que le but ici est de gérer les sessions entre ces deux frameworks Python pour vous donner plus de flexibilité.

Séances Flacon

Le microframework Flask prend en charge les sessions basées sur les cookies, ce qui signifie que toutes les données de session sont envoyées au client, codées en Base64 et signées de manière cryptographique. Mais en fait, nous ne sommes pas très intéressés par le support de session de Flask.

Ce dont nous avons besoin, c'est d'obtenir l'ID de session créé par Django et de le comparer au back-end Redis afin de nous assurer que la demande appartient à un utilisateur pré-signé. En résumé, le processus idéal serait (cela se synchronise avec le schéma ci-dessus) :

  • Nous récupérons l'ID de session Django à partir du cookie de l'utilisateur.
  • Si l'ID de session est trouvé dans Redis, nous renvoyons la session correspondant à cet ID.
  • Sinon, nous les redirigeons vers une page de connexion.

Il sera pratique d'avoir un décorateur pour vérifier ces informations et définir l' user_id actuel dans la variable g dans 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

Dans l'exemple ci-dessus, nous utilisons toujours le SessionStore que nous avons défini précédemment pour récupérer les données Django de Redis. Si la session a un _auth_user_id , nous retournons le contenu de la fonction view ; sinon, l'utilisateur est redirigé vers une page de connexion, comme nous le souhaitions.

Coller des choses ensemble

Afin de partager des cookies, je trouve pratique de démarrer Django et Flask via un serveur WSGI et de les coller ensemble. Dans cet exemple, j'ai utilisé 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)

Avec cela, Django servira sur "/" et Flask servira sur les points de terminaison "/ backend".

En conclusion

Plutôt que d'examiner Django contre Flask ou de vous encourager uniquement à apprendre le microframework Flask, j'ai fusionné Django et Flask, en les faisant partager les mêmes données de session pour l'authentification en déléguant la tâche à Django. Comme Django est livré avec de nombreux modules pour résoudre l'enregistrement, la connexion et la déconnexion des utilisateurs (pour n'en nommer que quelques-uns), la combinaison de ces deux frameworks vous fera gagner un temps précieux tout en vous offrant la possibilité de pirater un microframework gérable comme Flask.