دروس Django و Flask و Redis: إدارة جلسة تطبيق الويب بين أطر عمل Python

نشرت: 2022-03-11

دجانغو مقابل قارورة: عندما يكون Django هو الخيار الخاطئ

أحب استخدام Django في الكثير من مشاريعي الشخصية ومشاريع العملاء ، ومعظمها لتطبيقات الويب الكلاسيكية وتلك التي تتضمن قواعد البيانات العلائقية. ومع ذلك ، فإن Django ليس رصاصة فضية.

حسب التصميم ، فإن Django مرتبط بإحكام شديد مع ORM ، ونظام محرك القوالب ، وكائن الإعدادات. بالإضافة إلى أنه ليس مشروعًا جديدًا: فهو يحمل الكثير من الأمتعة ليظل متوافقًا مع الإصدارات السابقة.

يرى بعض مطوري Python أن هذا يمثل مشكلة كبيرة. يقولون إن Django ليس مرنًا بدرجة كافية ويتجنبه إن أمكن ، وبدلاً من ذلك ، استخدم إطار Python المصغر مثل Flask.

أنا لا أشارك هذا الرأي. يعد Django رائعًا عند استخدامه في المكان والزمان المناسبين ، حتى لو لم يتناسب مع كل مواصفات المشروع. كما يقول المانترا: "استخدم الأداة المناسبة للوظيفة".

(حتى عندما لا يكون المكان والزمان مناسبين ، أحيانًا يكون للبرمجة مع Django فوائد فريدة.)

في بعض الحالات ، قد يكون من الجيد بالفعل استخدام إطار خفيف الوزن (مثل Flask). في كثير من الأحيان ، تبدأ هذه الإطارات المصغرة في التألق عندما تدرك مدى سهولة اختراقها.

أطر دقيقة للإنقاذ

في عدد قليل من مشاريع عملائي ، ناقشنا التخلي عن Django والانتقال إلى إطار مصغر ، عادةً عندما يرغب العملاء في القيام ببعض الأشياء الشيقة (في حالة واحدة ، على سبيل المثال ، تضمين ZeroMQ في كائن التطبيق) والمشروع الأهداف تبدو أكثر صعوبة في تحقيقها مع Django.

بشكل عام ، أجد Flask مفيدًا في:

  • خلفيات REST API بسيطة
  • التطبيقات التي لا تتطلب الوصول إلى قاعدة البيانات
  • تطبيقات الويب المستندة إلى NoSQL
  • تطبيقات الويب بمتطلبات محددة للغاية ، مثل تكوينات عناوين URL المخصصة

في الوقت نفسه ، تطلب تطبيقنا تسجيل المستخدم والمهام الشائعة الأخرى التي حلها Django منذ سنوات. نظرًا لوزنها الخفيف ، فإن Flask لا يأتي مع نفس مجموعة الأدوات.

ظهر السؤال: هل Django صفقة كل شيء أو لا شيء؟

ظهر السؤال: هل Django صفقة كل شيء أو لا شيء؟ هل يجب أن نسقطها بالكامل من المشروع ، أم يمكننا تعلم دمجها مع مرونة الإطارات المصغرة الأخرى أو الأطر التقليدية؟ هل يمكننا انتقاء واختيار القطع التي نريد استخدامها وتجنب الآخرين؟

هل يمكننا الحصول على أفضل ما في العالمين؟ أقول نعم ، خاصة عندما يتعلق الأمر بإدارة الجلسة.

(ناهيك عن وجود الكثير من المشاريع لأصحاب العمل المستقلين في Django).

الآن درس Python: مشاركة جلسات Django

الهدف من هذا المنشور هو تفويض مهام مصادقة المستخدم وتسجيله إلى Django ، مع استخدام Redis لمشاركة جلسات المستخدم مع أطر أخرى. يمكنني التفكير في بعض السيناريوهات التي يكون فيها شيء من هذا القبيل مفيدًا:

  • تحتاج إلى تطوير واجهة برمجة تطبيقات REST بشكل منفصل عن تطبيق Django ولكنك ترغب في مشاركة بيانات الجلسة.
  • لديك مكون محدد قد تحتاج إلى استبداله لاحقًا أو توسيع نطاقه لسبب ما وما زلت بحاجة إلى بيانات الجلسة.

في هذا البرنامج التعليمي ، سأستخدم Redis لمشاركة الجلسات بين إطارين (في هذه الحالة ، Django و Flask). في الإعداد الحالي ، سأستخدم SQLite لتخزين معلومات المستخدم ، ولكن يمكنك ربط الواجهة الخلفية بقاعدة بيانات NoSQL (أو بديل قائم على SQL) إذا لزم الأمر.

فهم الجلسات

لمشاركة الجلسات بين Django و Flask ، نحتاج إلى معرفة القليل عن كيفية تخزين Django لمعلومات الجلسة. مستندات Django جيدة جدًا ، لكنني سأقدم بعض المعلومات الأساسية للاكتمال.

أصناف إدارة الجلسة

بشكل عام ، يمكنك اختيار إدارة بيانات جلسة تطبيق Python بإحدى طريقتين:

  • الجلسات القائمة على ملفات تعريف الارتباط : في هذا السيناريو ، لا يتم تخزين بيانات الجلسة في مخزن بيانات في النهاية الخلفية. بدلاً من ذلك ، يتم تسلسلها وتوقيعها (باستخدام SECRET_KEY) وإرسالها إلى العميل. عندما يرسل العميل تلك البيانات مرة أخرى ، يتم التحقق من سلامتها من أجل العبث ويتم إلغاء تسلسلها مرة أخرى على الخادم.

  • الجلسات المستندة إلى التخزين : في هذا السيناريو ، لا يتم إرسال بيانات الجلسة نفسها إلى العميل. بدلاً من ذلك ، يتم إرسال جزء صغير فقط (مفتاح) للإشارة إلى هوية المستخدم الحالي ، المخزنة في متجر الجلسة.

في مثالنا ، نحن مهتمون أكثر بالسيناريو الأخير: نريد تخزين بيانات جلستنا في النهاية الخلفية ثم التحقق منها في Flask. يمكن فعل الشيء نفسه في السابق ، ولكن كما تذكر وثائق Django ، هناك بعض المخاوف بشأن أمان الطريقة الأولى.

سير العمل العام

سيكون سير العمل العام للتعامل مع الجلسة وإدارتها مماثلاً لهذا الرسم التخطيطي:

رسم تخطيطي يوضح إدارة جلسات المستخدم بين Flask و Django باستخدام Redis.

دعنا نتصفح مشاركة الجلسة بمزيد من التفاصيل:

  1. عندما يأتي طلب جديد ، فإن الخطوة الأولى هي إرساله من خلال البرامج الوسيطة المسجلة في مكدس Django. نحن مهتمون هنا SessionMiddleware والتي ، كما قد تتوقع ، تتعلق بإدارة الجلسات والتعامل معها:

     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)

    في هذا المقتطف ، يستحوذ Django على SessionEngine المسجل (سنصل إليه قريبًا) ، ويستخرج SESSION_COOKIE_NAME من request ( sessionid ، افتراضيًا) وينشئ نسخة جديدة من SessionEngine المحدد للتعامل مع تخزين الجلسة.

  • في وقت لاحق (بعد معالجة عرض المستخدم ، ولكن لا يزال في مكدس البرامج الوسيطة) ، يستدعي محرك الجلسة طريقة الحفظ الخاصة به لحفظ أي تغييرات في مخزن البيانات. (أثناء معالجة العرض ، ربما قام المستخدم بتغيير بعض الأشياء داخل الجلسة ، على سبيل المثال ، بإضافة قيمة جديدة إلى كائن الجلسة مع request.session .) ثم يتم إرسال SESSION_COOKIE_NAME إلى العميل. ها هي النسخة المبسطة:

     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

نحن مهتمون بشكل خاص SessionEngine ، والتي سنستبدلها بشيء لتخزين البيانات وتحميلها من وإلى نهاية Redis الخلفية.

لحسن الحظ ، هناك عدد قليل من المشاريع التي تتعامل مع هذا بالفعل من أجلنا. هذا مثال من redis_sessions_fork . انتبه جيدًا إلى طرق save load ، والتي تمت كتابتها لتخزين الجلسة وتحميلها (على التوالي) في 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)

من المهم فهم كيفية عمل هذه الفئة حيث سنحتاج إلى تنفيذ شيء مشابه على Flask لتحميل بيانات الجلسة. دعنا نلقي نظرة فاحصة على مثال 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}

من السهل جدًا فهم واجهة متجر الجلسة ، ولكن هناك الكثير مما يحدث تحت الغطاء. يجب أن نحفر أعمق قليلاً حتى نتمكن من تنفيذ شيء مشابه على Flask.

ملاحظة: قد تسأل ، "لماذا لا تقوم فقط بنسخ محرك الجلسة إلى Flask؟" القول اسهل من الفعل. كما ناقشنا في البداية ، فإن Django مقترن بإحكام بكائن الإعدادات الخاص به ، لذلك لا يمكنك فقط استيراد بعض وحدات Django واستخدامها دون أي عمل إضافي.

جلسة Django (De-) التسلسل

كما قلت ، يقوم Django بالكثير من العمل لإخفاء تعقيد تخزين جلسته. دعنا نتحقق من مفتاح Redis المخزن في المقتطفات أعلاه:

 >>> store.session_key u"ery3j462ezmmgebbpwjajlxjxmvt5adu"

الآن ، دعنا نستعلم عن هذا المفتاح على redis-cli:

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

ما نراه هنا هو سلسلة طويلة جدًا مشفرة باستخدام Base64. لفهم الغرض منه ، نحتاج إلى إلقاء نظرة على فئة SessionBase من Django لمعرفة كيفية التعامل معها:

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

تقوم طريقة التشفير أولاً بتسلسل البيانات باستخدام جهاز التسلسل المسجل الحالي. بمعنى آخر ، فإنه يحول الجلسة إلى سلسلة ، والتي يمكن تحويلها لاحقًا إلى جلسة (انظر إلى وثائق SESSION_SERIALIZER لمزيد من المعلومات). بعد ذلك ، يقوم بتجزئة البيانات المتسلسلة ويستخدم هذه التجزئة لاحقًا كتوقيع للتحقق من سلامة بيانات الجلسة. أخيرًا ، تقوم بإرجاع زوج البيانات هذا إلى المستخدم كسلسلة مشفرة باستخدام Base64.

بالمناسبة: قبل الإصدار 1.6 ، تخلف Django عن استخدام pickle لتسلسل بيانات الجلسة. بسبب مخاوف أمنية ، فإن طريقة التسلسل الافتراضية هي الآن django.contrib.sessions.serializers.JSONSerializer .

ترميز جلسة مثال

دعونا نرى عملية إدارة الجلسة قيد التنفيذ. هنا ، سيكون قاموس الجلسة الخاص بنا عبارة عن عدد وعدد صحيح ، ولكن يمكنك أن تتخيل كيف يمكن تعميم ذلك على جلسات المستخدم الأكثر تعقيدًا.

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

نتيجة طريقة المتجر (u'ZmUxOTY… == ') عبارة عن سلسلة مشفرة تحتوي على جلسة المستخدم التسلسلية وتجزئتها . عندما نفك تشفيرها ، فإننا بالفعل نستعيد كل من التجزئة ('fe1964e ...') والجلسة ( {"count":1} ).

لاحظ أن طريقة فك التشفير تتحقق للتأكد من أن التجزئة صحيحة لتلك الجلسة ، مما يضمن سلامة البيانات عندما نذهب لاستخدامها في Flask. في حالتنا ، لسنا قلقين للغاية بشأن التلاعب بجلستنا من جانب العميل للأسباب التالية:

  • نحن لا نستخدم الجلسات القائمة على ملفات تعريف الارتباط ، أي أننا لا نرسل جميع بيانات المستخدم إلى العميل.

  • في Flask ، سنحتاج إلى SessionStore للقراءة فقط والذي سيخبرنا إذا كان هناك مفتاح معين أم لا ويعيد البيانات المخزنة.

يمتد إلى القارورة

بعد ذلك ، دعنا ننشئ نسخة مبسطة من محرك جلسة Redis (قاعدة بيانات) للعمل مع Flask. سنستخدم نفس SessionStore (المحدد أعلاه) كفئة أساسية ، لكننا سنحتاج إلى إزالة بعض وظائفها ، على سبيل المثال ، التحقق من التوقيعات السيئة أو تعديل الجلسات. نحن مهتمون أكثر بـ SessionStore للقراءة فقط والذي سيقوم بتحميل بيانات الجلسة المحفوظة من Django. دعنا نرى كيف يأتي معًا:

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

نحتاج فقط إلى طريقة load لأنها تنفيذ للقراءة فقط للتخزين. هذا يعني أنه لا يمكنك تسجيل الخروج مباشرة من Flask ؛ بدلاً من ذلك ، قد ترغب في إعادة توجيه هذه المهمة إلى Django. تذكر أن الهدف هنا هو إدارة الجلسات بين هذين الإطارين في Python لمنحك مزيدًا من المرونة.

جلسات القارورة

يدعم الإطار المصغر Flask الجلسات المستندة إلى ملفات تعريف الارتباط ، مما يعني أنه يتم إرسال جميع بيانات الجلسة إلى العميل ، بترميز Base64 وتوقيعه بشكل مشفر. لكن في الواقع ، لسنا مهتمين جدًا بدعم جلسة Flask.

ما نحتاجه هو الحصول على معرف الجلسة الذي أنشأه Django والتحقق منه مقابل نهاية Redis الخلفية حتى نتمكن من التأكد من أن الطلب ينتمي إلى مستخدم موقّع مسبقًا. باختصار ، ستكون العملية المثالية (تتزامن مع الرسم التخطيطي أعلاه):

  • نحصل على معرف جلسة Django من ملف تعريف ارتباط المستخدم.
  • إذا تم العثور على معرف الجلسة في Redis ، فإننا نعيد الجلسة المطابقة لهذا المعرف.
  • إذا لم يكن كذلك ، فإننا نعيد توجيههم إلى صفحة تسجيل الدخول.

سيكون من السهل أن يكون لديك مصمم للتحقق من تلك المعلومات وتعيين user_id الحالي في المتغير g في 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

في المثال أعلاه ، ما زلنا نستخدم SessionStore الذي حددناه مسبقًا لجلب بيانات Django من Redis. إذا كانت الجلسة تحتوي على _auth_user_id ، فإننا نعيد المحتوى من وظيفة العرض ؛ خلافًا لذلك ، تتم إعادة توجيه المستخدم إلى صفحة تسجيل الدخول ، تمامًا كما أردنا.

لصق الأشياء معًا

لمشاركة ملفات تعريف الارتباط ، أجد أنه من المناسب بدء تشغيل Django و Flask عبر خادم WSGI ولصقهما معًا. في هذا المثال ، استخدمت 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)

مع ذلك ، سيعمل Django على "/" وسيعمل Flask على نقاط النهاية "/ backend".

ختاما

بدلاً من فحص Django مقابل Flask أو تشجيعك فقط على تعلم الإطار المصغر للقارورة ، قمت بلحام Django و Flask معًا ، وحملهما على مشاركة نفس بيانات الجلسة للمصادقة من خلال تفويض المهمة إلى Django. نظرًا لأن Django يشحن مع الكثير من الوحدات النمطية لحل تسجيل المستخدم وتسجيل الدخول والخروج (على سبيل المثال لا الحصر) ، فإن الجمع بين هذين الإطارين سيوفر لك وقتًا ثمينًا بينما يوفر لك الفرصة لاختراق إطار صغير يمكن التحكم فيه مثل Flask.