Samouczek Django, Flask i Redis: Zarządzanie sesją aplikacji internetowych między frameworkami Pythona

Opublikowany: 2022-03-11

Django kontra Flask: Kiedy Django jest złym wyborem

Uwielbiam i używam Django w wielu moich osobistych i klienckich projektach, głównie w bardziej klasycznych aplikacjach internetowych i tych dotyczących relacyjnych baz danych. Jednak Django nie jest srebrną kulą.

Z założenia Django jest bardzo ściśle powiązane z ORM, Systemem Silnika Szablonów i obiektem Ustawień. Co więcej, nie jest to nowy projekt: przenosi dużo bagażu, aby zachować kompatybilność wsteczną.

Niektórzy programiści Pythona postrzegają to jako poważny problem. Mówią, że Django nie jest wystarczająco elastyczne i jeśli to możliwe, unikaj go, a zamiast tego używa mikroframeworków Pythona, takich jak Flask.

Nie podzielam tej opinii. Django jest świetne, gdy jest używane w odpowiednim miejscu i czasie, nawet jeśli nie pasuje do każdej specyfikacji projektu. Jak mówi mantra: „Użyj odpowiedniego narzędzia do pracy”.

(Nawet jeśli nie jest to odpowiednie miejsce i czas, czasami programowanie w Django może przynieść wyjątkowe korzyści.)

W niektórych przypadkach może być naprawdę fajnie użyć lżejszego frameworka (takiego jak Flask). Często te mikroframeworki zaczynają błyszczeć, gdy zdajesz sobie sprawę, jak łatwo je włamać.

Mikroframeworki na ratunek

W kilku moich projektach klienckich omawialiśmy rezygnację z Django i przejście na mikroframeworki, zazwyczaj gdy klienci chcą zrobić coś ciekawego (w jednym przypadku, na przykład osadzenie ZeroMQ w obiekcie aplikacji) i projekt cele wydają się trudniejsze do osiągnięcia z Django.

Bardziej ogólnie uważam, że Flask jest przydatny do:

  • Proste backendy REST API
  • Aplikacje niewymagające dostępu do bazy danych
  • Aplikacje internetowe oparte na NoSQL
  • Aplikacje internetowe o bardzo specyficznych wymaganiach, np. konfiguracje niestandardowych adresów URL

Jednocześnie nasza aplikacja wymagała rejestracji użytkownika i innych typowych zadań, które Django rozwiązało lata temu. Biorąc pod uwagę niewielką wagę, Flask nie jest dostarczany z tym samym zestawem narzędzi.

Pojawiło się pytanie: czy Django to układ typu „wszystko albo nic”?

Pojawiło się pytanie: czy Django to układ typu „wszystko albo nic”? Czy całkowicie odrzucić go z projektu, czy możemy nauczyć się łączyć go z elastycznością innych mikroframeworków lub tradycyjnych frameworków? Czy możemy wybrać elementy, których chcemy użyć, a unikać innych?

Czy możemy mieć to, co najlepsze z obu światów? Mówię tak, zwłaszcza jeśli chodzi o zarządzanie sesjami.

(Nie wspominając o tym, że istnieje wiele projektów dla freelancerów Django.)

Teraz samouczek Pythona: Udostępnianie sesji Django

Celem tego posta jest delegowanie zadań związanych z uwierzytelnianiem i rejestracją użytkowników do Django, przy jednoczesnym wykorzystaniu Redis do współdzielenia sesji użytkowników z innymi frameworkami. Przychodzi mi do głowy kilka scenariuszy, w których coś takiego przydałoby się:

  • Musisz opracować API REST niezależnie od aplikacji Django, ale chcesz udostępniać dane sesji.
  • Masz określony komponent, który z jakiegoś powodu może wymagać późniejszej wymiany lub skalowania, a mimo to nadal potrzebujesz danych sesji.

W tym samouczku użyję Redis do współdzielenia sesji między dwoma frameworkami (w tym przypadku Django i Flask). W obecnej konfiguracji będę używał SQLite do przechowywania informacji o użytkownikach, ale jeśli zajdzie taka potrzeba, możesz połączyć swój back-end z bazą danych NoSQL (lub alternatywą opartą na SQL).

Zrozumienie sesji

Aby udostępniać sesje między Django i Flask, musimy wiedzieć trochę o tym, jak Django przechowuje informacje o swoich sesjach. Dokumentacja Django jest całkiem dobra, ale przedstawię trochę tła dla kompletności.

Odmiany zarządzania sesją

Ogólnie rzecz biorąc, możesz wybrać zarządzanie danymi sesji aplikacji Python na jeden z dwóch sposobów:

  • Sesje oparte na plikach cookie : w tym scenariuszu dane sesji nie są przechowywane w magazynie danych na zapleczu. Zamiast tego jest serializowany, podpisany (za pomocą SECRET_KEY) i wysyłany do klienta. Gdy klient odeśle te dane, ich integralność jest sprawdzana pod kątem manipulacji i jest ponownie deserializowana na serwerze.

  • Sesje oparte na magazynie : w tym scenariuszu same dane sesji nie są wysyłane do klienta. Zamiast tego wysyłana jest tylko niewielka część (klucz), aby wskazać tożsamość bieżącego użytkownika, przechowywaną w magazynie sesji.

W naszym przykładzie bardziej interesuje nas ten drugi scenariusz: chcemy, aby nasze dane sesji były przechowywane na zapleczu, a następnie sprawdzane w Flask. To samo można zrobić w pierwszym, ale jak wspomina dokumentacja Django, istnieją pewne obawy dotyczące bezpieczeństwa pierwszej metody.

Ogólny przepływ pracy

Ogólny przebieg obsługi i zarządzania sesjami będzie podobny do tego diagramu:

Diagram przedstawiający zarządzanie sesjami użytkowników między Flask i Django przy użyciu Redis.

Przyjrzyjmy się bardziej szczegółowo udostępnianiu sesji:

  1. Gdy nadejdzie nowe żądanie, pierwszym krokiem jest przesłanie go przez zarejestrowane oprogramowanie pośredniczące w stosie Django. Interesuje nas tutaj klasa SessionMiddleware , która, jak można się spodziewać, jest związana z zarządzaniem i obsługą sesji:

     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)

    W tym fragmencie Django pobiera zarejestrowany SessionEngine (dojdziemy do tego wkrótce), wyodrębnia SESSION_COOKIE_NAME z request ( domyślnie sessionid ) i tworzy nową instancję wybranego SessionEngine do obsługi przechowywania sesji.

  • Później (po przetworzeniu widoku użytkownika, ale nadal w stosie oprogramowania pośredniego) aparat sesji wywołuje swoją metodę save, aby zapisać wszelkie zmiany w magazynie danych. (Podczas obsługi widoku użytkownik mógł zmienić kilka rzeczy w ramach sesji, np. dodając nową wartość do obiektu sesji z request.session .) Następnie do klienta wysyłana jest SESSION_COOKIE_NAME . Oto uproszczona wersja:

     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

Szczególnie interesuje nas klasa SessionEngine , którą zastąpimy czymś do przechowywania i ładowania danych do i z zaplecza Redis.

Na szczęście jest kilka projektów, które już to za nas obsługują. Oto przykład z redis_sessions_fork . Zwróć szczególną uwagę na metody save i load , które są napisane tak, aby (odpowiednio) przechowywać i ładować sesję do iz 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)

Ważne jest, aby zrozumieć, jak działa ta klasa, ponieważ będziemy musieli zaimplementować coś podobnego w Flask, aby załadować dane sesji. Przyjrzyjmy się bliżej przykładowi 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}

Interfejs sklepu sesyjnego jest dość łatwy do zrozumienia, ale pod maską dużo się dzieje. Powinniśmy sięgnąć trochę głębiej, aby móc zaimplementować coś podobnego na Flasku.

Uwaga: możesz zapytać: „Dlaczego po prostu nie skopiować SessionEngine do Flask?” Łatwiej powiedzieć niż zrobić. Jak wspomnieliśmy na początku, Django jest ściśle powiązane z obiektem Settings, więc nie można po prostu zaimportować jakiegoś modułu Django i używać go bez dodatkowej pracy.

(De-)serializacja sesji Django

Jak powiedziałem, Django wykonuje dużo pracy, aby zamaskować złożoność przechowywania sesji. Sprawdźmy klucz Redis, który jest przechowywany w powyższych fragmentach:

 >>> store.session_key u"ery3j462ezmmgebbpwjajlxjxmvt5adu"

Teraz zapytajmy o ten klucz w redis-cli:

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

Widzimy tutaj bardzo długi ciąg zakodowany w Base64. Aby zrozumieć jego przeznaczenie, musimy przyjrzeć się klasie SessionBase w Django, aby zobaczyć, jak jest obsługiwana:

 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 encode najpierw serializuje dane z bieżącym zarejestrowanym serializatorem. Innymi słowy, konwertuje sesję na ciąg znaków, który może później przekonwertować z powrotem na sesję (więcej informacji znajdziesz w dokumentacji SESSION_SERIALIZER). Następnie szyfruje zserializowane dane i wykorzystuje je później jako podpis w celu sprawdzenia integralności danych sesji. Na koniec zwraca tę parę danych użytkownikowi jako ciąg zakodowany w Base64.

Przy okazji: przed wersją 1.6 Django domyślnie używało pickle do serializacji danych sesji. Ze względów bezpieczeństwa domyślną metodą serializacji jest teraz django.contrib.sessions.serializers.JSONSerializer .

Kodowanie przykładowej sesji

Zobaczmy proces zarządzania sesją w akcji. Tutaj nasz słownik sesji będzie po prostu liczbą i pewną liczbą całkowitą, ale możesz sobie wyobrazić, jak to uogólniłoby się na bardziej skomplikowane sesje użytkowników.

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

Wynikiem metody przechowywania (u'ZmUxOTY…==') jest zakodowany ciąg znaków zawierający zserializowaną sesję użytkownika i jej hash. Kiedy go zdekodujemy, rzeczywiście otrzymujemy zarówno hash ('fe1964e…') jak i sesję ( {"count":1} ).

Zwróć uwagę, że metoda dekodowania sprawdza, czy hash jest poprawny dla tej sesji, gwarantując integralność danych, gdy używamy ich w Flask. W naszym przypadku nie martwimy się zbytnio ingerencją w naszą sesję po stronie klienta, ponieważ:

  • Nie używamy sesji opartych na plikach cookie, tzn. nie wysyłamy do klienta wszystkich danych użytkownika.

  • W Flask będziemy potrzebować SessionStore tylko do odczytu, które poinformuje nas, czy dany klucz istnieje, czy nie i zwróci zapisane dane.

Rozszerzenie do Flask

Następnie utwórzmy uproszczoną wersję aparatu sesji Redis (bazy danych) do pracy z Flask. Użyjemy tego samego SessionStore (zdefiniowanego powyżej) jako klasy bazowej, ale będziemy musieli usunąć niektóre z jej funkcji, np. sprawdzanie pod kątem złych podpisów lub modyfikowanie sesji. Bardziej interesuje nas SessionStore tylko do odczytu, który załaduje dane sesji zapisane z Django. Zobaczmy, jak to się układa:

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

Potrzebujemy tylko metody load , ponieważ jest to implementacja magazynu tylko do odczytu. Oznacza to, że nie możesz wylogować się bezpośrednio z Flask; zamiast tego możesz chcieć przekierować to zadanie do Django. Pamiętaj, że celem jest zarządzanie sesjami między tymi dwoma frameworkami Pythona, aby zapewnić większą elastyczność.

Sesje kolbowe

Mikroframework Flask obsługuje sesje oparte na plikach cookie, co oznacza, że ​​wszystkie dane sesji są wysyłane do klienta zakodowane w Base64 i podpisane kryptograficznie. Ale tak naprawdę nie jesteśmy bardzo zainteresowani obsługą sesji Flask.

To, czego potrzebujemy, to uzyskanie identyfikatora sesji utworzonego przez Django i sprawdzenie go z back-endem Redis, aby mieć pewność, że żądanie należy do wstępnie podpisanego użytkownika. Podsumowując, idealnym procesem byłoby (to synchronizuje się z powyższym diagramem):

  • Pobieramy identyfikator sesji Django z pliku cookie użytkownika.
  • Jeśli identyfikator sesji zostanie znaleziony w Redis, zwracamy sesję pasującą do tego identyfikatora.
  • Jeśli nie, przekierowujemy ich na stronę logowania.

Przyda się dekorator, który sprawdzi te informacje i ustawi bieżący user_id użytkownika w zmiennej g w 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

W powyższym przykładzie nadal używamy zdefiniowanego wcześniej SessionStore do pobierania danych Django z Redis. Jeśli sesja ma _auth_user_id , zwracamy treść z funkcji widoku; w przeciwnym razie użytkownik zostanie przekierowany na stronę logowania, tak jak chcieliśmy.

Sklejanie rzeczy razem

Aby udostępniać pliki cookie, wygodnie jest uruchomić Django i Flask za pośrednictwem serwera WSGI i skleić je razem. W tym przykładzie użyłem 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)

Dzięki temu Django będzie serwował na „/”, a Flask na punktach końcowych „/backend”.

Na zakończenie

Zamiast sprawdzać Django kontra Flask lub zachęcać tylko do nauki mikroframeworka Flask, zespawałem ze sobą Django i Flask, zmuszając je do współdzielenia tych samych danych sesji w celu uwierzytelnienia, delegując zadanie do Django. Ponieważ Django dostarcza wiele modułów do rozwiązywania problemów związanych z rejestracją, logowaniem i wylogowaniem użytkowników (to tylko kilka), połączenie tych dwóch frameworków pozwoli zaoszczędzić cenny czas, jednocześnie zapewniając możliwość włamania się do łatwego w zarządzaniu mikroframeworku, takiego jak Flask.