Jeg elsker og bruker Django i mange av mine personlige og klientprosjekter, mest for mer klassiske webapplikasjoner og de som involverer relasjonsdatabaser. Imidlertid er Django ingen sølvkule.
Av design er Django veldig tett kombinert med ORM-, Template Engine System- og Settings-objektet. I tillegg er det ikke et nytt prosjekt: det har mye bagasje for å være bakoverkompatibel.
Noen Python-utviklere ser på dette som et stort problem. De sier at Django ikke er fleksibel nok, og unngå det hvis det er mulig, og bruk i stedet et Python-mikrorammer som Flask.
Jeg deler ikke den oppfatningen. Django er flott når den brukes i passende sted og tid , selv om det ikke passer inn hver prosjekt spesifikasjon Som mantraet sier: “Bruk riktig verktøy for jobben”.
(Selv når det ikke er riktig sted og tid, kan programmering med Django noen ganger ha unike fordeler.)
I noen tilfeller kan det virkelig være hyggelig å bruke et mer lett rammeverk (som f.eks Kolbe ). Ofte begynner disse mikrorammer å skinne når du skjønner hvor enkelt de er å hacke på.
I noen få av mine klientprosjekter har vi diskutert å gi opp Django og flytte til et mikrorammer, vanligvis når klientene vil gjøre noen interessante ting (i et tilfelle for eksempel å legge inn ZeroMQ i applikasjonsobjektet) og prosjektmålene virker vanskeligere å oppnå med Django.
Mer generelt synes jeg Flask er nyttig for:
Samtidig krevde appen vår brukerregistrering og andre vanlige oppgaver som Django løste for mange år siden. Med tanke på den lave vekten, kommer ikke Flask med samme verktøysett.
Spørsmålet dukket opp: er Django en alt-eller-ingenting-avtale?Spørsmålet dukket opp: er Django en alt-eller-ingenting-avtale? Bør vi droppe det helt fra prosjektet, eller kan vi lære å kombinere det med fleksibiliteten til andre mikrorammer eller tradisjonelle rammer? Kan vi plukke og velge brikkene vi vil bruke og unngå andre?
Kan vi få det beste fra begge verdener? Jeg sier ja, spesielt når det gjelder øktledelse.
(For ikke å nevne, det er mange prosjekter der ute for Django-frilansere.)
Målet med dette innlegget er å delegere oppgavene med brukerautentisering og registrering til Django, men bruk Redis til å dele brukersessioner med andre rammer. Jeg kan tenke på noen få scenarier der noe slikt ville være nyttig:
For denne opplæringen vil jeg bruke Redis å dele økter mellom to rammer (i dette tilfellet Django og Flask). I det nåværende oppsettet vil jeg bruke SQLite for å lagre brukerinformasjon, men du kan ha back-enden bundet til en NoSQL-database (eller et SQL-basert alternativ) om nødvendig.
For å dele økter mellom Django og Flask, må vi vite litt om hvordan Django lagrer øktinformasjonen. De Django dokumenterer er ganske bra, men jeg vil gi litt bakgrunn for fullstendighet.
Generelt kan du velge å administrere Python-appens øktdata på en av to måter:
Cookie-baserte økter : I dette scenariet lagres ikke øktdataene i et datalager på baksiden. I stedet serialiseres den, signeres (med SECRET_KEY) og sendes til klienten. Når klienten sender dataene tilbake, blir integriteten sjekket for manipulering, og de deserialiseres på nytt på serveren.
Lagringsbaserte økter : I dette scenariet er selve sesjonsdataene ikke sendt til klienten. I stedet sendes bare en liten del (en nøkkel) for å indikere identiteten til den nåværende brukeren, lagret i øktbutikken.
I vårt eksempel er vi mer interessert i det sistnevnte scenariet: vi vil at øktdataene våre skal lagres på baksiden og deretter sjekkes i Flask. Det samme kan gjøres i den tidligere, men som Django-dokumentasjonen nevner, er det noen bekymringer om sikkerheten av den første metoden.
Den generelle arbeidsflyten for økthåndtering og administrasjon vil være lik dette diagrammet:
La oss gå gjennom øktdelingen litt mer detaljert:
Når en ny forespørsel kommer inn, er det første trinnet å sende den gjennom den registrerte mellomvare i Django-stakken. Vi er interessert her i SessionMiddleware
klasse som, som du kanskje forventer, er relatert til øktadministrasjon og håndtering:
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)
I dette utdraget tar Django den registrerte SessionEngine
(vi kommer snart til det), trekker ut SESSION_COOKIE_NAME
fra request
(sessionid
, som standard) og oppretter en ny forekomst av den valgte SessionEngine
for å håndtere øktlagring.
Senere (etter at brukervisningen er behandlet, men fortsatt i mellomvarestakken), kaller øktmotoren lagringsmetoden for å lagre eventuelle endringer i datalageret. (Under visningshåndtering kan brukeren ha endret et par ting i økten, f.eks. Ved å legge til en ny verdi til øktobjektet med request.session
.) Deretter SESSION_COOKIE_NAME
blir sendt til klienten. Her er den forenklede versjonen:
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
Vi er spesielt interessert i SessionEngine
klasse, som vi vil erstatte med noe å lagre og laste inn data til og fra en Redis-back-end.
Heldigvis er det noen få prosjekter som allerede håndterer dette for oss. Her er et eksempel fra redis_sessions_fork . Vær nøye med save
og load
metoder, som er skrevet for å (henholdsvis) lagre og laste økten til og fra 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)
Det er viktig å forstå hvordan denne klassen fungerer, ettersom vi trenger å implementere noe lignende på Flask for å laste inn øktdata. La oss se nærmere på et REPL-eksempel:
>>> from django.conf import settings >>> from django.utils.importlib import import_module >>> engine = import_module(settings.SESSION_ENGINE) >>> engine.SessionStore() >>> store['count'] = 1 >>> store.save() >>> store.load() {u'count': 1}
Sesjonsbutikkens grensesnitt er ganske lett å forstå, men det skjer mye under panseret. Vi bør grave litt dypere slik at vi kan implementere noe lignende på Flask.
Merk: Du kan spørre: 'Hvorfor ikke bare kopiere SessionEngine til Flask?' Lettere sagt enn gjort. Som vi diskuterte i begynnelsen, er Django tett kombinert med innstillingsobjektet, så du kan ikke bare importere noen Django-modul og bruke den uten noe ekstra arbeid.
Som jeg gjør, gjør Django mye arbeid for å skjule kompleksiteten til øktlagringen. La oss sjekke Redis-nøkkelen som er lagret i utdragene ovenfor:
>>> store.session_key u'ery3j462ezmmgebbpwjajlxjxmvt5adu'
La oss spørre den nøkkelen på redis-cli:
redis 127.0.0.1:6379> get 'django_sessions:ery3j462ezmmgebbpwjajlxjxmvt5adu' 'ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ=='
Det vi ser her er veldig lenge, Base64-kodet streng. For å forstå formålet, må vi se på Djangos SessionBase
klasse for å se hvordan det håndteres:
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 {}
Kodemetoden serialiserer først dataene med den nåværende registrerte serialisereren. Med andre ord konverterer den økten til en streng, som den senere kan konvertere tilbake til en økt (se dokumentasjonen SESSION_SERIALIZER for mer). Deretter hasher den de serielle dataene og bruker denne hashen senere som en signatur for å kontrollere integriteten til øktdataene. Til slutt returnerer den dataparet til brukeren som en Base64-kodet streng.
Forresten: før versjon 1.6 brukte Django som standard å bruke sylteagurk for serialisering av sesjonsdata. På grunn av sikkerhetshensyn er standardiseringsmetoden nå django.contrib.sessions.serializers.JSONSerializer
.
La oss se prosessen for øktadministrasjon i aksjon. Her vil sesjonsordboken vår ganske enkelt være et antall og et heltall, men du kan forestille deg hvordan dette vil generalisere til mer kompliserte brukersessioner.
>>> store.encode({'count': 1}) u'ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ==' >>> base64.b64decode(encoded) 'fe1964e1d2cf8069d9f1823afd143400b6d3736f:{'count':1}'
Resultatet av butikkmetoden (u’ZmUxOTY ... == ’) er en kodet streng som inneholder den serielle brukerøkten og dens hash. Når vi dekoder det, får vi faktisk tilbake både hashen (‘fe1964e ...’) og økten ({'count':1}
).
Merk at dekodemetoden kontrollerer at hasjen er riktig for den økten, noe som garanterer integriteten til dataene når vi bruker dem i Flask. I vårt tilfelle er vi ikke så bekymret for at økten vår blir tuklet med på klientsiden fordi:
Vi bruker ikke informasjonskapselbaserte økter, dvs. vi sender ikke alle brukerdata til klienten.
På Flask trenger vi en skrivebeskyttet SessionStore
som vil fortelle oss om det finnes en gitt nøkkel eller ikke, og returnere de lagrede dataene.
La oss deretter lage en forenklet versjon av Redis-sesjonsmotoren (database) for å jobbe med Flask. Vi bruker det samme SessionStore
(definert ovenfor) som en basisklasse, men vi må fjerne noe av funksjonaliteten, for eksempel å sjekke for dårlige signaturer eller endre økter. Vi er mer interessert i en skrivebeskyttet SessionStore
som vil laste øktdata lagret fra Django. La oss se hvordan det kommer sammen:
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 (i.e., empty session). return {}
Vi trenger bare load
metode fordi det er en skrivebeskyttet implementering av lagringen. Det betyr at du ikke kan logge ut direkte fra Flask; i stedet vil du kanskje omdirigere denne oppgaven til Django. Husk at målet her er å administrere økter mellom disse to Python-rammene for å gi deg mer fleksibilitet.
Flask microframework støtter informasjonskapselbaserte økter, noe som betyr at alle øktdataene sendes til klienten, Base64-kodet og kryptografisk signert. Men faktisk er vi ikke veldig interessert i Flasks øktstøtte.
Det vi trenger er å få økt-ID-en opprettet av Django og sjekke den mot Redis-back-enden, slik at vi kan være sikre på at forespørselen tilhører en forhåndssignert bruker. Oppsummert vil den ideelle prosessen være (dette synkroniseres med diagrammet ovenfor):
Det vil være nyttig å ha en dekoratør som kan se etter informasjonen og angi gjeldende user_id
inn i g
variabel i kolbe:
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
I eksemplet ovenfor bruker vi fremdeles SessionStore
vi definerte tidligere å hente Django-dataene fra Redis. Hvis økten har en _auth_user_id
, returnerer vi innholdet fra visningsfunksjonen; Ellers blir brukeren omdirigert til en påloggingsside, akkurat som vi ønsket.
For å dele informasjonskapsler, synes jeg det er praktisk å starte Django og Flask via en WSGI serveren og lim dem sammen. I dette eksemplet har jeg brukt 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)
Med det vil Django tjene på “/” og Flask vil tjene på “/ backend” endepunkter.
I stedet for å undersøke Django versus Flask eller oppfordre deg til bare å lære Flask-mikroframverket, har jeg sveiset sammen Django og Flask, og fått dem til å dele de samme øktdataene for autentisering ved å delegere oppgaven til Django. Ettersom Django leveres med mange moduler for å løse brukerregistrering, pålogging og utlogging (bare for å nevne noen få), vil kombinasjon av disse to rammene spare deg for verdifull tid og gi deg muligheten til å hacke på et håndterbart mikrorammer som Flask.