Tutorial Django, Flask, dan Redis: Manajemen Sesi Aplikasi Web Antara Kerangka Kerja Python
Diterbitkan: 2022-03-11Django Versus Flask: Ketika Django adalah Pilihan yang Salah
Saya menyukai dan menggunakan Django dalam banyak proyek pribadi dan klien saya, sebagian besar untuk aplikasi web yang lebih klasik dan yang melibatkan basis data relasional. Namun, Django bukanlah peluru perak.
Secara desain, Django sangat erat digabungkan dengan objek ORM, Sistem Mesin Template, dan Pengaturannya. Plus, ini bukan proyek baru: ia membawa banyak barang bawaan agar tetap kompatibel.
Beberapa pengembang Python melihat ini sebagai masalah besar. Mereka mengatakan bahwa Django tidak cukup fleksibel dan menghindarinya jika memungkinkan dan, sebagai gantinya, menggunakan kerangka kerja mikro Python seperti Flask.
Saya tidak berbagi pendapat itu. Django sangat bagus ketika digunakan di tempat dan waktu yang tepat, bahkan jika itu tidak cocok dengan setiap spesifikasi proyek. Seperti mantranya: "Gunakan alat yang tepat untuk pekerjaan itu".
(Bahkan ketika itu bukan tempat dan waktu yang tepat, terkadang pemrograman dengan Django dapat memiliki manfaat yang unik.)
Dalam beberapa kasus, memang menyenangkan menggunakan kerangka kerja yang lebih ringan (seperti Flask). Seringkali, kerangka kerja mikro ini mulai bersinar ketika Anda menyadari betapa mudahnya mereka untuk diretas.
Microframeworks untuk Menyelamatkan
Dalam beberapa proyek klien saya, kami telah membahas menyerah pada Django dan pindah ke mikroframework, biasanya ketika klien ingin melakukan beberapa hal menarik (dalam satu kasus, misalnya, menyematkan ZeroMQ di objek aplikasi) dan proyek tujuan tampaknya lebih sulit dicapai dengan Django.
Secara umum, menurut saya Flask berguna untuk:
- Backend REST API sederhana
- Aplikasi yang tidak memerlukan akses database
- Aplikasi web berbasis NoSQL
- Aplikasi web dengan persyaratan yang sangat spesifik, seperti konfigurasi URL khusus
Pada saat yang sama, aplikasi kami memerlukan pendaftaran pengguna dan tugas umum lainnya yang diselesaikan Django bertahun-tahun yang lalu. Mengingat bobotnya yang ringan, Flask tidak datang dengan toolkit yang sama.
Muncul pertanyaan: apakah Django kesepakatan semua-atau-tidak sama sekali? Haruskah kita menghapusnya sepenuhnya dari proyek, atau dapatkah kita belajar menggabungkannya dengan fleksibilitas kerangka kerja mikro atau kerangka tradisional lainnya? Bisakah kita memilih dan memilih bagian yang ingin kita gunakan dan menghindari yang lain?
Bisakah kita memiliki yang terbaik dari kedua dunia? Saya katakan ya, terutama dalam hal manajemen sesi.
(Belum lagi, ada banyak proyek di luar sana untuk pekerja lepas Django.)
Sekarang Tutorial Python: Berbagi Sesi Django
Tujuan dari posting ini adalah untuk mendelegasikan tugas-tugas otentikasi dan registrasi pengguna ke Django, namun menggunakan Redis untuk berbagi sesi pengguna dengan kerangka kerja lain. Saya dapat memikirkan beberapa skenario di mana sesuatu seperti ini akan berguna:
- Anda perlu mengembangkan REST API secara terpisah dari aplikasi Django Anda tetapi ingin berbagi data sesi.
- Anda memiliki komponen tertentu yang mungkin perlu diganti nanti atau diskalakan karena alasan tertentu dan masih memerlukan data sesi.
Untuk tutorial ini, saya akan menggunakan Redis untuk berbagi sesi antara dua kerangka kerja (dalam hal ini, Django dan Flask). Dalam pengaturan saat ini, saya akan menggunakan SQLite untuk menyimpan informasi pengguna, tetapi Anda dapat memiliki back-end Anda terikat ke database NoSQL (atau alternatif berbasis SQL) jika perlu.
Memahami Sesi
Untuk berbagi sesi antara Django dan Flask, kita perlu mengetahui sedikit tentang bagaimana Django menyimpan informasi sesinya. Dokumen Django cukup bagus, tetapi saya akan memberikan beberapa latar belakang untuk kelengkapannya.
Varietas Manajemen Sesi
Secara umum, Anda dapat memilih untuk mengelola data sesi aplikasi Python dengan salah satu dari dua cara:
Sesi berbasis cookie : Dalam skenario ini, data sesi tidak disimpan di penyimpanan data di back-end. Sebaliknya, itu serial, ditandatangani (dengan SECRET_KEY), dan dikirim ke klien. Ketika klien mengirim data itu kembali, integritasnya diperiksa untuk gangguan dan deserialized lagi di server.
Sesi berbasis penyimpanan : Dalam skenario ini, data sesi itu sendiri tidak dikirim ke klien. Sebaliknya, hanya sebagian kecil yang dikirim (kunci) untuk menunjukkan identitas pengguna saat ini, yang disimpan di penyimpanan sesi.
Dalam contoh kami, kami lebih tertarik pada skenario terakhir: kami ingin data sesi kami disimpan di back-end dan kemudian diperiksa di Flask. Hal yang sama dapat dilakukan pada yang pertama, tetapi seperti yang disebutkan oleh dokumentasi Django, ada beberapa kekhawatiran tentang keamanan metode pertama.
Alur Kerja Umum
Alur kerja umum penanganan dan manajemen sesi akan serupa dengan diagram ini:
Mari kita telusuri berbagi sesi dengan sedikit lebih detail:
Saat permintaan baru masuk, langkah pertama adalah mengirimkannya melalui middleware terdaftar di tumpukan Django. Kami tertarik di sini di kelas
SessionMiddleware
yang, seperti yang Anda harapkan, terkait dengan manajemen dan penanganan sesi: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)
Dalam cuplikan ini, Django mengambil
SessionEngine
terdaftar (kita akan segera mendapatkannya), mengekstrakSESSION_COOKIE_NAME
darirequest
(sessionid
, secara default) dan membuat instance baru dariSessionEngine
yang dipilih untuk menangani penyimpanan sesi.
Kemudian (setelah tampilan pengguna diproses, tetapi masih di tumpukan middleware), mesin sesi memanggil metode penyimpanannya untuk menyimpan perubahan apa pun ke penyimpanan data. (Selama penanganan tampilan, pengguna mungkin telah mengubah beberapa hal dalam sesi, misalnya dengan menambahkan nilai baru ke objek sesi dengan
request.session
.) Kemudian,SESSION_COOKIE_NAME
dikirim ke klien. Berikut versi yang disederhanakan: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
Kami sangat tertarik dengan kelas SessionEngine
, yang akan kami ganti dengan sesuatu untuk menyimpan dan memuat data ke dan dari back-end Redis.
Untungnya, ada beberapa proyek yang sudah menangani ini untuk kami. Berikut ini contoh dari redis_sessions_fork . Perhatikan baik-baik metode save
dan load
, yang ditulis untuk (masing-masing) menyimpan dan memuat sesi ke dan dari 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)
Sangat penting untuk memahami bagaimana kelas ini beroperasi karena kita perlu mengimplementasikan sesuatu yang serupa pada Flask untuk memuat data sesi. Mari kita lihat lebih dekat dengan contoh 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}
Antarmuka session store cukup mudah untuk dipahami, tetapi ada banyak hal yang terjadi di bawah tenda. Kita harus menggali lebih dalam agar kita bisa mengimplementasikan sesuatu yang serupa di Flask.

Catatan: Anda mungkin bertanya, “Mengapa tidak menyalin SessionEngine ke Flask saja?” Lebih mudah diucapkan daripada dilakukan. Seperti yang telah kita bahas di awal, Django digabungkan erat dengan objek Pengaturannya, jadi Anda tidak bisa hanya mengimpor beberapa modul Django dan menggunakannya tanpa pekerjaan tambahan apa pun.
Sesi Django (De-)Serialization
Seperti yang saya katakan, Django melakukan banyak pekerjaan untuk menutupi kerumitan penyimpanan sesinya. Mari kita periksa kunci Redis yang disimpan dalam cuplikan di atas:
>>> store.session_key u"ery3j462ezmmgebbpwjajlxjxmvt5adu"
Sekarang, mari kita kueri kunci itu di redis-cli:
redis 127.0.0.1:6379> get "django_sessions:ery3j462ezmmgebbpwjajlxjxmvt5adu" "ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ=="
Apa yang kita lihat di sini adalah string yang disandikan Base64 yang sangat panjang. Untuk memahami tujuannya, kita perlu melihat kelas SessionBase
Django untuk melihat bagaimana penanganannya:
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 {}
Metode encode pertama-tama membuat serialisasi data dengan serializer terdaftar saat ini. Dengan kata lain, ini mengubah sesi menjadi string, yang nantinya dapat diubah kembali menjadi sesi (lihat dokumentasi SESSION_SERIALIZER untuk lebih lanjut). Kemudian, ia meng-hash data serial dan menggunakan hash ini nanti sebagai tanda tangan untuk memeriksa integritas data sesi. Terakhir, ia mengembalikan pasangan data itu ke pengguna sebagai string yang disandikan Base64.
Omong-omong: sebelum versi 1.6, Django secara default menggunakan acar untuk serialisasi data sesi. Karena masalah keamanan, metode serialisasi default sekarang adalah django.contrib.sessions.serializers.JSONSerializer
.
Mengkodekan Sesi Contoh
Mari kita lihat proses manajemen sesi beraksi. Di sini, kamus sesi kami hanya akan menjadi hitungan dan bilangan bulat, tetapi Anda dapat membayangkan bagaimana ini akan digeneralisasi ke sesi pengguna yang lebih rumit.
>>> store.encode({'count': 1}) u'ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ==' >>> base64.b64decode(encoded) 'fe1964e1d2cf8069d9f1823afd143400b6d3736f:{"count":1}'
Hasil dari metode penyimpanan (u'ZmUxOTY…==') adalah string yang dikodekan yang berisi sesi pengguna serial dan hashnya. Saat kami mendekodenya, kami memang mendapatkan kembali hash ('fe1964e…') dan sesi ( {"count":1}
).
Perhatikan bahwa metode decode memeriksa untuk memastikan bahwa hash benar untuk sesi itu, menjamin integritas data saat kita menggunakannya di Flask. Dalam kasus kami, kami tidak terlalu khawatir tentang sesi kami dirusak di sisi klien karena:
Kami tidak menggunakan sesi berbasis cookie, yaitu, kami tidak mengirimkan semua data pengguna ke klien.
Di Flask, kita memerlukan
SessionStore
hanya-baca yang akan memberi tahu kita apakah kunci yang diberikan ada atau tidak dan mengembalikan data yang disimpan.
Memperluas ke Flask
Selanjutnya, mari kita buat versi sederhana dari mesin sesi Redis (database) untuk bekerja dengan Flask. Kami akan menggunakan SessionStore
yang sama (didefinisikan di atas) sebagai kelas dasar, tetapi kami harus menghapus beberapa fungsinya, misalnya, memeriksa tanda tangan yang buruk atau memodifikasi sesi. Kami lebih tertarik pada SessionStore
hanya-baca yang akan memuat data sesi yang disimpan dari Django. Mari kita lihat bagaimana hal itu datang bersama-sama:
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 {}
Kami hanya memerlukan metode load
karena ini adalah implementasi penyimpanan hanya-baca. Itu artinya Anda tidak bisa langsung logout dari Flask; sebagai gantinya, Anda mungkin ingin mengarahkan tugas ini ke Django. Ingat, tujuannya di sini adalah untuk mengelola sesi antara dua kerangka kerja Python ini untuk memberi Anda lebih banyak fleksibilitas.
Sesi Flask
Microframework Flask mendukung sesi berbasis cookie, yang berarti semua data sesi dikirim ke klien, dikodekan Base64 dan ditandatangani secara kriptografis. Tapi sebenarnya, kami tidak terlalu tertarik dengan dukungan sesi Flask.
Yang kita perlukan adalah mendapatkan ID sesi yang dibuat oleh Django dan memeriksanya pada back-end Redis sehingga kita dapat yakin bahwa permintaan tersebut milik pengguna yang telah ditandatangani sebelumnya. Singkatnya, proses yang ideal adalah (ini disinkronkan dengan diagram di atas):
- Kami mengambil ID sesi Django dari cookie pengguna.
- Jika ID sesi ditemukan di Redis, kami mengembalikan sesi yang cocok dengan ID itu.
- Jika tidak, kami mengarahkan mereka ke halaman login.
Akan berguna untuk memiliki dekorator untuk memeriksa informasi itu dan mengatur user_id
saat ini ke dalam variabel g
di 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
Dalam contoh di atas, kita masih menggunakan SessionStore
yang kita definisikan sebelumnya untuk mengambil data Django dari Redis. Jika sesi memiliki _auth_user_id
, kami mengembalikan konten dari fungsi tampilan; jika tidak, pengguna akan diarahkan ke halaman login, seperti yang kita inginkan.
Merekatkan Benda Bersama
Untuk berbagi cookie, saya merasa nyaman untuk memulai Django dan Flask melalui server WSGI dan merekatkannya bersama-sama. Dalam contoh ini, saya telah menggunakan 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)
Dengan itu, Django akan melayani pada “/” dan Flask akan melayani pada titik akhir “/ backend”.
Kesimpulannya
Daripada memeriksa Django versus Flask atau mendorong Anda hanya untuk mempelajari kerangka kerja mikro Flask, saya telah menyatukan Django dan Flask, membuat mereka berbagi data sesi yang sama untuk otentikasi dengan mendelegasikan tugas ke Django. Karena Django dikirimkan dengan banyak modul untuk menyelesaikan pendaftaran pengguna, login, dan logout (hanya untuk beberapa nama), menggabungkan dua kerangka kerja ini akan menghemat waktu Anda yang berharga sambil memberi Anda kesempatan untuk meretas pada kerangka kerja mikro yang dapat dikelola seperti Flask.