Tutorial de Django, Flask y Redis: administración de sesiones de aplicaciones web entre marcos de Python

Publicado: 2022-03-11

Django versus Flask: cuando Django es la elección equivocada

Amo y uso Django en muchos de mis proyectos personales y de clientes, principalmente para aplicaciones web más clásicas y aquellas que involucran bases de datos relacionales. Sin embargo, Django no es una panacea.

Por diseño, Django está estrechamente relacionado con su objeto ORM, Template Engine System y Settings. Además, no es un proyecto nuevo: lleva mucho equipaje para seguir siendo compatible con versiones anteriores.

Algunos desarrolladores de Python ven esto como un problema importante. Dicen que Django no es lo suficientemente flexible y lo evitan si es posible y, en su lugar, usan un microframework de Python como Flask.

No comparto esa opinión. Django es excelente cuando se usa en el lugar y el momento adecuados, incluso si no se ajusta a todas las especificaciones del proyecto. Como dice el mantra: "Utilice la herramienta adecuada para el trabajo".

(Incluso cuando no es el lugar y el momento correctos, a veces programar con Django puede tener beneficios únicos).

En algunos casos, puede ser bueno usar un marco más ligero (como Flask). A menudo, estos microframeworks comienzan a brillar cuando te das cuenta de lo fácil que es piratearlos.

Microframeworks al rescate

En algunos de mis proyectos de clientes, hemos discutido renunciar a Django y pasar a un microframework, generalmente cuando los clientes quieren hacer cosas interesantes (en un caso, por ejemplo, incrustar ZeroMQ en el objeto de la aplicación) y el proyecto Los objetivos parecen más difíciles de lograr con Django.

En términos más generales, encuentro que Flask es útil para:

  • Backends API REST simples
  • Aplicaciones que no requieren acceso a la base de datos
  • Aplicaciones web basadas en NoSQL
  • Aplicaciones web con requisitos muy específicos, como configuraciones de URL personalizadas

Al mismo tiempo, nuestra aplicación requería el registro de usuarios y otras tareas comunes que Django resolvió hace años. Dado su peso ligero, Flask no viene con el mismo conjunto de herramientas.

Surgió la pregunta: ¿Django es un trato de todo o nada?

Surgió la pregunta: ¿Django es un trato de todo o nada? ¿Deberíamos eliminarlo por completo del proyecto o podemos aprender a combinarlo con la flexibilidad de otros microframeworks o frameworks tradicionales? ¿Podemos elegir las piezas que queremos usar y evitar otras?

¿Podemos tener lo mejor de ambos mundos? Digo que sí, especialmente cuando se trata de la gestión de sesiones.

(Sin mencionar que hay muchos proyectos para freelancers de Django).

Ahora el tutorial de Python: compartir sesiones de Django

El objetivo de esta publicación es delegar las tareas de autenticación y registro de usuarios a Django, pero usar Redis para compartir sesiones de usuarios con otros marcos. Puedo pensar en algunos escenarios en los que algo como esto sería útil:

  • Debe desarrollar una API REST por separado de su aplicación Django pero desea compartir datos de sesión.
  • Tiene un componente específico que puede necesitar ser reemplazado más adelante o ampliado por algún motivo y aún necesita datos de sesión.

Para este tutorial, usaré Redis para compartir sesiones entre dos marcos (en este caso, Django y Flask). En la configuración actual, usaré SQLite para almacenar la información del usuario, pero puede vincular su back-end a una base de datos NoSQL (o una alternativa basada en SQL) si es necesario.

Comprender las sesiones

Para compartir sesiones entre Django y Flask, necesitamos saber un poco sobre cómo Django almacena su información de sesión. Los documentos de Django son bastante buenos, pero proporcionaré algunos antecedentes para completarlos.

Variedades de gestión de sesiones

En general, puede elegir administrar los datos de sesión de su aplicación de Python de una de dos maneras:

  • Sesiones basadas en cookies : en este escenario, los datos de la sesión no se almacenan en un almacén de datos en el back-end. En su lugar, se serializa, firma (con SECRET_KEY) y se envía al cliente. Cuando el cliente devuelve esos datos, se verifica su integridad en busca de manipulación y se deserializa nuevamente en el servidor.

  • Sesiones basadas en almacenamiento : en este escenario, los datos de la sesión en sí no se envían al cliente. En su lugar, solo se envía una pequeña parte (una clave) para indicar la identidad del usuario actual, almacenada en el almacén de sesiones.

En nuestro ejemplo, estamos más interesados ​​en el último escenario: queremos que los datos de nuestra sesión se almacenen en el back-end y luego se registren en Flask. Se podría hacer lo mismo con el primero, pero como menciona la documentación de Django, existen algunas preocupaciones sobre la seguridad del primer método.

El flujo de trabajo general

El flujo de trabajo general de manejo y gestión de sesiones será similar a este diagrama:

Un diagrama que muestra la gestión de sesiones de usuario entre Flask y Django usando Redis.

Veamos el uso compartido de sesiones con un poco más de detalle:

  1. Cuando llega una nueva solicitud, el primer paso es enviarla a través del middleware registrado en la pila de Django. Estamos interesados ​​aquí en la clase SessionMiddleware que, como es de esperar, está relacionada con la gestión y el manejo de sesiones:

     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)

    En este fragmento, Django toma el SessionEngine registrado (lo abordaremos pronto), extrae el SESSION_COOKIE_NAME de la request ( sessionid , por defecto) y crea una nueva instancia del SessionEngine seleccionado para manejar el almacenamiento de la sesión.

  • Más tarde (después de que se procese la vista del usuario, pero aún en la pila de middleware), el motor de sesión llama a su método de guardado para guardar cualquier cambio en el almacén de datos. (Durante el manejo de la vista, el usuario puede haber cambiado algunas cosas dentro de la sesión, por ejemplo, agregando un nuevo valor al objeto de la sesión con request.session ). Luego, SESSION_COOKIE_NAME se envía al cliente. Aquí está la versión simplificada:

     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

Estamos particularmente interesados ​​en la clase SessionEngine , que reemplazaremos con algo para almacenar y cargar datos hacia y desde un back-end de Redis.

Afortunadamente, hay algunos proyectos que ya manejan esto por nosotros. Aquí hay un ejemplo de redis_sessions_fork . Preste mucha atención a los métodos de save y load , que están escritos para (respectivamente) almacenar y cargar la sesión en y desde 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)

Es importante comprender cómo funciona esta clase, ya que necesitaremos implementar algo similar en Flask para cargar los datos de la sesión. Echemos un vistazo más de cerca con un ejemplo de 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}

La interfaz de la tienda de sesión es bastante fácil de entender, pero están sucediendo muchas cosas debajo del capó. Deberíamos profundizar un poco más para poder implementar algo similar en Flask.

Nota: puede preguntar: "¿Por qué no simplemente copiar SessionEngine en Flask?" Es más fácil decirlo que hacerlo. Como discutimos al principio, Django está estrechamente relacionado con su objeto de configuración, por lo que no puede simplemente importar algún módulo de Django y usarlo sin ningún trabajo adicional.

Sesión Django (Des-)Serialización

Como dije, Django hace mucho trabajo para enmascarar la complejidad del almacenamiento de su sesión. Verifiquemos la clave Redis que está almacenada en los fragmentos anteriores:

 >>> store.session_key u"ery3j462ezmmgebbpwjajlxjxmvt5adu"

Ahora, consultemos esa clave en redis-cli:

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

Lo que vemos aquí es una cadena muy larga codificada en Base64. Para comprender su propósito, debemos observar la clase SessionBase de Django para ver cómo se maneja:

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

El método de codificación primero serializa los datos con el serializador registrado actual. En otras palabras, convierte la sesión en una cadena, que luego puede volver a convertir en una sesión (consulte la documentación de SESSION_SERIALIZER para obtener más información). Luego, procesa los datos serializados y usa este hash más tarde como una firma para verificar la integridad de los datos de la sesión. Finalmente, devuelve ese par de datos al usuario como una cadena codificada en Base64.

Por cierto: antes de la versión 1.6, Django usaba por defecto pickle para la serialización de los datos de la sesión. Debido a problemas de seguridad, el método de serialización predeterminado ahora es django.contrib.sessions.serializers.JSONSerializer .

Codificación de una sesión de ejemplo

Veamos el proceso de gestión de sesiones en acción. Aquí, nuestro diccionario de sesión será simplemente un conteo y algún número entero, pero puede imaginar cómo esto se generalizaría a sesiones de usuario más complicadas.

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

El resultado del método de almacenamiento (u'ZmUxOTY…==') es una cadena codificada que contiene la sesión de usuario serializada y su hash. Cuando lo decodificamos, recuperamos tanto el hash ('fe1964e…') como la sesión ( {"count":1} ).

Tenga en cuenta que el método de decodificación verifica que el hash sea correcto para esa sesión, lo que garantiza la integridad de los datos cuando vamos a usarlos en Flask. En nuestro caso, no nos preocupa demasiado que nuestra sesión sea manipulada en el lado del cliente porque:

  • No utilizamos sesiones basadas en cookies, es decir, no enviamos todos los datos del usuario al cliente.

  • En Flask, necesitaremos un SessionStore de solo lectura que nos diga si la clave dada existe o no y devolverá los datos almacenados.

Extendiendo al matraz

A continuación, creemos una versión simplificada del motor de sesión de Redis (base de datos) para que funcione con Flask. Usaremos el mismo SessionStore (definido anteriormente) como clase base, pero necesitaremos eliminar algunas de sus funciones, por ejemplo, verificar firmas incorrectas o modificar sesiones. Estamos más interesados ​​en un SessionStore de solo lectura que cargará los datos de la sesión guardados desde Django. Veamos cómo se 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 {}

Solo necesitamos el método de load porque es una implementación de solo lectura del almacenamiento. Eso significa que no puede cerrar sesión directamente desde Flask; en su lugar, es posible que desee redirigir esta tarea a Django. Recuerde, el objetivo aquí es administrar sesiones entre estos dos marcos de Python para brindarle más flexibilidad.

Sesiones de matraz

El micromarco Flask admite sesiones basadas en cookies, lo que significa que todos los datos de la sesión se envían al cliente, codificados en Base64 y firmados criptográficamente. Pero en realidad, no estamos muy interesados ​​en el soporte de sesión de Flask.

Lo que necesitamos es obtener el ID de sesión creado por Django y compararlo con el back-end de Redis para asegurarnos de que la solicitud pertenece a un usuario prefirmado. En resumen, el proceso ideal sería (esto se sincroniza con el diagrama anterior):

  • Tomamos la ID de sesión de Django de la cookie del usuario.
  • Si el ID de sesión se encuentra en Redis, devolvemos la sesión que coincide con ese ID.
  • Si no, los redirigimos a una página de inicio de sesión.

Será útil tener un decorador para verificar esa información y establecer el user_id actual en la variable g en 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

En el ejemplo anterior, todavía usamos SessionStore que definimos anteriormente para obtener los datos de Django de Redis. Si la sesión tiene un _auth_user_id , devolvemos el contenido de la función de vista; de lo contrario, el usuario es redirigido a una página de inicio de sesión, tal como queríamos.

Pegar cosas juntas

Para compartir cookies, me parece conveniente iniciar Django y Flask a través de un servidor WSGI y unirlos. En este ejemplo, he usado 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 eso, Django servirá en "/" y Flask servirá en los puntos finales "/ backend".

En conclusión

En lugar de examinar Django versus Flask o alentarlo solo a aprender el micromarco de Flask, he fusionado Django y Flask, haciendo que compartan los mismos datos de sesión para la autenticación al delegar la tarea a Django. Como Django viene con muchos módulos para resolver el registro, inicio y cierre de sesión de usuarios (solo por nombrar algunos), la combinación de estos dos marcos le ahorrará un tiempo valioso y le brindará la oportunidad de piratear un micromarco manejable como Flask.