Tutorial Django, Flask e Redis: Gerenciamento de sessão de aplicativo da Web entre frameworks Python

Publicados: 2022-03-11

Django Versus Flask: Quando Django é a escolha errada

Eu amo e uso o Django em muitos dos meus projetos pessoais e de clientes, principalmente para aplicações web mais clássicas e aquelas envolvendo bancos de dados relacionais. No entanto, o Django não é uma bala de prata.

Por design, o Django é fortemente acoplado com seu ORM, Template Engine System e objeto Settings. Além disso, não é um projeto novo: carrega muita bagagem para permanecer compatível com versões anteriores.

Alguns desenvolvedores Python veem isso como um grande problema. Eles dizem que o Django não é flexível o suficiente e evite-o se possível e, em vez disso, use um microframework Python como o Flask.

Eu não compartilho dessa opinião. O Django é ótimo quando usado no local e tempo apropriados, mesmo que não se encaixe em todas as especificações do projeto. Como diz o mantra: “Use a ferramenta certa para o trabalho”.

(Mesmo quando não é o lugar e a hora certos, às vezes programar com Django pode ter benefícios únicos.)

Em alguns casos, pode ser bom usar um framework mais leve (como o Flask). Muitas vezes, esses microframeworks começam a brilhar quando você percebe como eles são fáceis de hackear.

Microframeworks para o resgate

Em alguns dos meus projetos de clientes, discutimos desistir do Django e migrar para um microframework, normalmente quando os clientes querem fazer algumas coisas interessantes (em um caso, por exemplo, incorporar ZeroMQ no objeto de aplicativo) e o projeto metas parecem mais difíceis de alcançar com o Django.

De maneira mais geral, acho o Flask útil para:

  • Back-ends de API REST simples
  • Aplicativos que não exigem acesso ao banco de dados
  • Aplicativos Web baseados em NoSQL
  • Aplicativos da Web com requisitos muito específicos, como configurações de URL personalizadas

Ao mesmo tempo, nosso aplicativo exigia o registro do usuário e outras tarefas comuns que o Django resolveu anos atrás. Dado seu peso leve, o Flask não vem com o mesmo kit de ferramentas.

A questão surgiu: o Django é um negócio de tudo ou nada?

A questão surgiu: o Django é um negócio de tudo ou nada? Devemos abandoná-lo completamente do projeto ou podemos aprender a combiná-lo com a flexibilidade de outros microframeworks ou frameworks tradicionais? Podemos escolher as peças que queremos usar e evitar outras?

Podemos ter o melhor dos dois mundos? Eu digo que sim, especialmente quando se trata de gerenciamento de sessões.

(Sem mencionar, existem muitos projetos para freelancers do Django.)

Agora o Tutorial Python: Compartilhando Sessões do Django

O objetivo deste post é delegar as tarefas de autenticação e registro de usuários ao Django, mas usar o Redis para compartilhar sessões de usuários com outros frameworks. Posso pensar em alguns cenários em que algo assim seria útil:

  • Você precisa desenvolver uma API REST separadamente do seu aplicativo Django, mas deseja compartilhar dados de sessão.
  • Você tem um componente específico que pode precisar ser substituído posteriormente ou dimensionado por algum motivo e ainda precisa de dados de sessão.

Para este tutorial, usarei o Redis para compartilhar sessões entre dois frameworks (neste caso, Django e Flask). Na configuração atual, usarei o SQLite para armazenar informações do usuário, mas você pode ter seu back-end vinculado a um banco de dados NoSQL (ou uma alternativa baseada em SQL), se necessário.

Entendendo as Sessões

Para compartilhar sessões entre o Django e o Flask, precisamos saber um pouco sobre como o Django armazena suas informações de sessão. Os documentos do Django são muito bons, mas fornecerei alguns antecedentes para completar.

Variedades de gerenciamento de sessão

Geralmente, você pode optar por gerenciar os dados de sessão do seu aplicativo Python de duas maneiras:

  • Sessões baseadas em cookies : neste cenário, os dados da sessão não são armazenados em um armazenamento de dados no back-end. Em vez disso, é serializado, assinado (com uma SECRET_KEY) e enviado ao cliente. Quando o cliente envia esses dados de volta, sua integridade é verificada quanto à adulteração e é desserializada novamente no servidor.

  • Sessões baseadas em armazenamento : nesse cenário, os dados da sessão em si não são enviados ao cliente. Em vez disso, apenas uma pequena parte é enviada (uma chave) para indicar a identidade do usuário atual, armazenada no armazenamento de sessão.

Em nosso exemplo, estamos mais interessados ​​no último cenário: queremos que nossos dados de sessão sejam armazenados no back-end e depois verificados no Flask. A mesma coisa poderia ser feita no primeiro método, mas como a documentação do Django menciona, existem algumas preocupações sobre a segurança do primeiro método.

O fluxo de trabalho geral

O fluxo de trabalho geral de manipulação e gerenciamento de sessão será semelhante a este diagrama:

Um diagrama mostrando o gerenciamento de sessões de usuário entre Flask e Django usando Redis.

Vamos analisar o compartilhamento de sessão com um pouco mais de detalhes:

  1. Quando uma nova requisição chega, o primeiro passo é enviá-la através do middleware registrado na pilha do Django. Estamos interessados ​​aqui na classe SessionMiddleware que, como você pode esperar, está relacionada ao gerenciamento e tratamento de sessões:

     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)

    Neste trecho, o Django pega o SessionEngine registrado (chegaremos a isso em breve), extrai o SESSION_COOKIE_NAME da request ( sessionid , por padrão) e cria uma nova instância do SessionEngine selecionado para lidar com o armazenamento de sessão.

  • Mais tarde (após a visualização do usuário ser processada, mas ainda na pilha de middleware), o mecanismo de sessão chama seu método save para salvar quaisquer alterações no armazenamento de dados. (Durante o tratamento da visualização, o usuário pode ter alterado algumas coisas dentro da sessão, por exemplo, adicionando um novo valor ao objeto de sessão com request.session .) Em seguida, o SESSION_COOKIE_NAME é enviado ao cliente. Aqui está a versão 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 interessados ​​na classe SessionEngine , que substituiremos por algo para armazenar e carregar dados de e para um back-end do Redis.

Felizmente, existem alguns projetos que já lidam com isso para nós. Aqui está um exemplo de redis_sessions_fork . Preste muita atenção aos métodos save e load , que são escritos para (respectivamente) armazenar e carregar a sessão no 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 entender como essa classe está operando, pois precisaremos implementar algo semelhante no Flask para carregar os dados da sessão. Vamos dar uma olhada mais de perto com um exemplo 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}

A interface da loja de sessões é bem fácil de entender, mas há muita coisa acontecendo nos bastidores. Devemos ir um pouco mais fundo para que possamos implementar algo semelhante no Flask.

Nota: Você pode perguntar: “Por que não apenas copiar o SessionEngine para o Flask?” Mais fácil falar do que fazer. Como discutimos no início, o Django está fortemente acoplado ao seu objeto Settings, então você não pode simplesmente importar algum módulo Django e usá-lo sem nenhum trabalho adicional.

Serialização de Sessão Django

Como eu disse, o Django faz muito trabalho para mascarar a complexidade de seu armazenamento de sessão. Vamos verificar a chave Redis armazenada nos snippets acima:

 >>> store.session_key u"ery3j462ezmmgebbpwjajlxjxmvt5adu"

Agora, vamos consultar essa chave no redis-cli:

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

O que vemos aqui é uma string muito longa codificada em Base64. Para entender seu propósito, precisamos olhar para a classe SessionBase do Django para ver como ela é tratada:

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

O método de codificação primeiro serializa os dados com o serializador registrado atual. Em outras palavras, ele converte a sessão em uma string, que pode ser convertida posteriormente em uma sessão (veja a documentação do SESSION_SERIALIZER para mais). Em seguida, ele faz o hash dos dados serializados e usa esse hash posteriormente como uma assinatura para verificar a integridade dos dados da sessão. Por fim, ele retorna esse par de dados ao usuário como uma string codificada em Base64.

A propósito: antes da versão 1.6, o Django usava como padrão o pickle para serialização de dados de sessão. Devido a questões de segurança, o método de serialização padrão agora é django.contrib.sessions.serializers.JSONSerializer .

Codificando uma sessão de exemplo

Vamos ver o processo de gerenciamento de sessão em ação. Aqui, nosso dicionário de sessão será simplesmente uma contagem e algum número inteiro, mas você pode imaginar como isso se generalizaria para sessões de usuário mais complicadas.

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

O resultado do método de armazenamento (u'ZmUxOTY…==') é uma string codificada contendo a sessão de usuário serializada e seu hash. Quando o decodificamos, de fato retornamos o hash ('fe1964e…') e a sessão ( {"count":1} ).

Observe que o método decode verifica se o hash está correto para aquela sessão, garantindo a integridade dos dados quando formos utilizá-lo no Flask. No nosso caso, não estamos muito preocupados com a violação de nossa sessão no lado do cliente porque:

  • Não estamos usando sessões baseadas em cookies, ou seja, não estamos enviando todos os dados do usuário para o cliente.

  • No Flask, precisaremos de um SessionStore somente leitura que nos dirá se uma determinada chave existe ou não e retornará os dados armazenados.

Estendendo para o Frasco

Em seguida, vamos criar uma versão simplificada do mecanismo de sessão do Redis (banco de dados) para trabalhar com o Flask. Usaremos o mesmo SessionStore (definido acima) como uma classe base, mas precisaremos remover algumas de suas funcionalidades, por exemplo, verificar assinaturas incorretas ou modificar sessões. Estamos mais interessados ​​em um SessionStore somente leitura que carregará os dados de sessão salvos do Django. Vamos ver como fica:

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

Precisamos apenas do método load porque é uma implementação somente leitura do armazenamento. Isso significa que você não pode sair diretamente do Flask; em vez disso, você pode querer redirecionar esta tarefa para o Django. Lembre-se, o objetivo aqui é gerenciar sessões entre essas duas estruturas Python para oferecer mais flexibilidade.

Sessões de Frasco

O microframework Flask suporta sessões baseadas em cookies, o que significa que todos os dados da sessão são enviados ao cliente, codificados em Base64 e assinados criptograficamente. Mas, na verdade, não estamos muito interessados ​​no suporte de sessão do Flask.

O que precisamos é obter o ID da sessão criado pelo Django e verificá-lo no back-end do Redis para que possamos ter certeza de que a solicitação pertence a um usuário pré-assinado. Em resumo, o processo ideal seria (isso é sincronizado com o diagrama acima):

  • Pegamos o ID da sessão do Django do cookie do usuário.
  • Se o ID da sessão for encontrado no Redis, retornaremos a sessão correspondente a esse ID.
  • Caso contrário, redirecionamos para uma página de login.

Será útil ter um decorador para verificar essas informações e definir o user_id atual na variável g no 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

No exemplo acima, ainda estamos usando o SessionStore que definimos anteriormente para buscar os dados do Django do Redis. Se a sessão tiver um _auth_user_id , retornamos o conteúdo da função view; caso contrário, o usuário é redirecionado para uma página de login, exatamente como desejávamos.

Colando coisas juntas

Para compartilhar cookies, acho conveniente iniciar o Django e o Flask por meio de um servidor WSGI e colá-los. Neste exemplo, usei 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)

Com isso, o Django servirá em “/” e o Flask servirá em endpoints “/backend”.

Para concluir

Em vez de examinar Django versus Flask ou incentivá-lo a apenas aprender o microframework Flask, eu juntei Django e Flask, fazendo com que eles compartilhassem os mesmos dados de sessão para autenticação, delegando a tarefa ao Django. Como o Django vem com muitos módulos para resolver o registro, login e logout do usuário (só para citar alguns), a combinação desses dois frameworks economizará um tempo valioso enquanto oferece a oportunidade de hackear um microframework gerenciável como o Flask.