14  الجلسة

الأصل في بروتوكول HTTP أنه ناقل عديم الحالة (Stateless)؛ بمعنى أن كل طلب فيه مستقلٌّ عما قبله. فلا يجوز أن يعتمد الطلب الثاني على نفوذ الطلب الأول. بل يجب أن يتضمَّن كل طلبٍ جميع المعلومات اللازمة، إضافة إلى تحديد ما يريد.

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

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

ومن منافع تصميم البروتوكول بهذه الآلية أمران:

الأول: ذاكرة أوسع: لا حاجة للخادم أن يحتفظ بمعلومات كل العملاء في وقت واحد؛ بل إن كل عميلٍ معه ما يلزم لخدمته. فهذا يوسِّع على الخادم ذاكرته لخدمة عدد لا محدود من العملاء.

الثاني: توزيع الجهد: يمكن لأكثر من خادم أن يخدم نفس العميل بين طلب وآخر؛ وبالتالي فإنه ثمة خادم وسيط عكسي (Reverse Proxy) يلعب دوْر موزِّع الجهد (Load Balancer) يعيِّن الخادم المناسب بحسب فراغهم وشغلهم (وغالبًا ما يكون التوزيع العشوائي كافيًا).

وتم الاصطلاح على: ملفات تعريف الارتباط (Cookies) وهي كلمة أصلها (Magic Cookies) وهي تشير لدوْرة يحصُلُ فيها الاستفادة من الشيء، بتمريره من الطرف الأول للثاني ثم رده كما هو من غير تعديل عليه؛ وذلك كالوثائق الحكوميَّة؛ حيث لا يجوز للمواطنين العبث فيها، وإنما يحتفظون فيها ويبرزونها عند حاجة موظفي الدولة إليها؛ ثم هم يعدلون عليها ويسلموها إلى المواطن مرة أخرى ليحتفظ فيها، وهكذا دواليك.

وهذا يعني أن الخادم يأمر المتصفِّح بحفظ معلومات معيَّنة يتم إرسالُها في كل طلب. ولا يجوز للعميل التعديل عليها. ويُمكن إلزامه بذلك عن طريق توقيع (Signature) مخصوص لضمان السلامة (Integrity). وهذا ما سنعرفه في درس الحماية -إن شاء الله-.

الـCookies في starlette

نتصور وجود صفحة لضبط:

  1. الوضع الداكن (dark_mode)
  2. واللغة (language)

سيكون القالب بهذا الشكل:

<h1>Settings</h1>

<form method="POST" action="/settings">
    <div>
        <label for="dark_mode">Dark Mode</label>
        <input
            type="checkbox"
            name="dark_mode"
            {% if dark_mode == "on" %}checked{% endif %}>
    </div>

    <div>
        <label for="language">Language</label>
        <select
            id="language"
            name="language">
            {% for language_code in languages.keys() %}
                <option value="{{ language_code }}" {% if language_code == selected_language_code %}selected{% endif %}>{{ languages[language_code] }}</option>
            {% endfor %}
        </select>
    </div>

    <button type="submit">Save</button>
</form>

<a href="/settings/demo">Settings Demo</a>

فتكون هذه نتيجة الصفحة:

لاحظ أننا خصصنا الاستمارة بشيء مختلف وهو method وتشير هذه إلى تخصيص نوع الطلب الذي يتم إرساله على نفس المسار. وبذلك نستطيع أن نتعامل معه بحسبه:

  1. الطريقة request.method == "GET" وهي الأصل؛ إذ تعني أن الصفحة يتم زيارتها
  2. الطريقة request.method == "POST" تعني أن استمارة المدخلات تم تسليمها

سنضع القطعة كاملة، ثم نفصلها في الأسفل:

from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import RedirectResponse
from starlette.routing import Route
from starlette.templating import Jinja2Templates


templates = Jinja2Templates(directory="src/views")

languages = {
    "en": "English",
    "ar": "Arabic",
}

async def settings(request: Request):
    if request.method == "GET":
        dark_mode = request.cookies.get("dark_mode", "off")
        language_code = request.cookies.get("language", "en")

        return templates.TemplateResponse("visitor/settings.html", {
            "request": request,
            "dark_mode": dark_mode,
            "languages": languages,
            "selected_language_code": language_code,
        })

    elif request.method == "POST":
        form = await request.form()
        dark_mode = form.get("dark_mode", "off")
        language_code = form.get("language", "en")

        # Prepare the response as a redirect to the demo page
        response = RedirectResponse(url="/settings/demo", status_code=303)
        response.set_cookie(key="dark_mode", value=dark_mode)
        response.set_cookie(key="language", value=language_code)
        return response


def settings_demo(request: Request):
    return templates.TemplateResponse("visitor/settings_demo.html", {
        "request": request,
        "dark_mode": request.cookies.get("dark_mode", "off"),
        "language": request.cookies.get("language", "en"),
        "languages": languages,
    })

routes = [
    Route("/settings", settings, methods=["GET", "POST"]),
    Route("/settings/demo", settings_demo, methods=["GET"]),
]

app = Starlette(
    debug=True,
    routes=routes,
)

القطعة الأولى: فتعمل عند الزيارة. فهي تأخذ البيانات المخزَّنة في الـCookies فإن لم تجد شيئًا فإنها تفترض القيمة المقابلة: request.cookes.get(value, default). بعد ذلك تملأ القالب بالبيانات وتجيب به:

if request.method == "GET":
    dark_mode = request.cookies.get("dark_mode", "off")
    language_code = request.cookies.get("language", "en")

    return templates.TemplateResponse("visitor/settings.html", {
        "request": request,
        "dark_mode": dark_mode,
        "languages": languages,
        "selected_language_code": language_code,
    })

أما القطعة الثانية: فتعمل عند إرسال الاستمارة بمدخلاتها التي فيها عند الضغط على زر Save كما في القالب. والمعالجة تقضي بقراءة الاستمارة في form = await request.form().

فأما await وكذلك async def المذكورة قبل ذلك في التعريف، فسيأتي الكلام عنها في حينه إن شاء الله. لكن هي أمور زائدة لا تؤثر في النتيجة.

بعد ذلك يتم قراءة البيانات: form.get، ثم ننشئ الجواب الذي هو تحويل المستخدم لصفحة العرض (demo)، ونعدل فيه الـCookies، ثم نرجع به في نهاية الدالة: return response:

elif request.method == "POST":
    form = await request.form()
    dark_mode = form.get("dark_mode", "off")
    language_code = form.get("language", "en")

    # Prepare the response as a redirect to the demo page
    response = RedirectResponse(url="/settings/demo", status_code=303)
    response.set_cookie(key="dark_mode", value=dark_mode)
    response.set_cookie(key="language", value=language_code)
    return response

أما صفحة العرض، فهذا قالبها:

<h1>Settings Demo</h1>

<p>Dark Mode: {{ dark_mode }}</p>
<p>Language: {{ languages[language] }}</p>

والمنطق المتصل بها هو في الدالة التالية في بايثون؛ إذْ تقرأ ما يكون في الـCookies (وتفترض قيَم ابتدائية إن لم يوجد)، وتعرضه:

def settings_demo(request: Request):
    return templates.TemplateResponse("visitor/settings_demo.html", {
        "request": request,
        "dark_mode": request.cookies.get("dark_mode", "off"),
        "language": request.cookies.get("language", "en"),
        "languages": languages,
    })

لتظهر النتيجة:

ثم ربط ذلك بالمسارات:

routes = [
    Route("/settings", settings, methods=["GET", "POST"]),
    Route("/settings/demo", settings_demo, methods=["GET"]),
]

ثم استعمالها في إنشاء التطبيق:

app = Starlette(
    debug=True,
    routes=routes,
)

وأخيراً تشغيله:

uvicorn main:app --reload

يمكنك معرفة جميع الـCookies المخزنة عن طريق أدوات المطور في المتصفح (DevTools):

  1. ستخدم اختصار لوحة المفاتيح:
    • في وندوز وليكنس: F12 أو Ctrl + Shift + I
    • في ماك أو إس: Cmd + Option + I
  2. انتقل إلى لوحة التطبيق (Application)
  3. ابحث في الجانب الأيسر عن التخزين (Storage) وستجد من ضمن القائمة Cookies

وبهذا يتبيَّن لنا آلية حفظ بيانات تطبيق الويب المختصَّة بجلسة عميل ما.