اذهب إلى المحتوى

إنشاء مدونة باستخدام Express (الجزء 4): إدارة الجلسات


محمد فوّاز عرابي

لا قيمة لمدوّنتنا إن لم يكن باستطاعتنا كتابة التّدوينات الجديدة ونشرها، لذا علينا إنشاء صفحة تُتيح لنا (لنا فقط) كتابة تدوينة جديدة وحفظها في قاعدة البيانات. لكن أوّل ما يتبادر إلى الذّهن تساؤل عن الكيفيّة الّتي نستطيع أن نمنع فيها الزّائر من إضافة التّدوينات، إذ كيف يستطيع المتصفّح والخادوم التّفريق بين صاحب المدوّنة وزائرها؟

ملايين المواقع على الويب تقدّم محتوىً مخصّصًا لكلّ مستخدم، إذا عدنا لمثال فيس بوك وطرحنا السؤال ذاته: كيف يعرض فيس بوك نشرة أخبار خاصّة بكلّ مستخدم؟ بالطّبع لكلّ مستخدم حساب في الموقع محميّ بكلمة مرور، لكن ما الذي يحدث بين المتصفّح والخادوم ويجعل الخادوم يُرسل الصّفحة الخاصّة بالمستخدم إليه دون غيره؟

إن كنت سمعت من قبل بالكعكات (cookies) ولم تعرف ما علاقتها بالويب، فقد حان الوقت لنعرف ما هي وكيف تستخدم.

الكعكات (Cookies)

الكعكات هي أجزاء صغيرة من البيانات تخزّن في المتصفّح وتنتقل بينه وبين الخادوم مع كلّ طلب إلى هذا الخادوم (كترويسة Heading في طلب HTTP)، يمكن للخادوم أن يأمر المتصفّح بحفظ بيانات جديدة ضمن الكعكات، ويمكن للمتصفّح منع تخزين هذه البيانات بأمر المستخدم أو حذفها متى شاء. تُستخدم الكعكات بشكل شائع لتخزين "الجلسة" (session)، وهي الطّريقة الّتي يتذكّر بها الخادوم هذا المتصفّح بين الطّلب والآخر بحيث يستطيع تمييزه من بين الطّلبات الّتي تصله من حواسيب مختلفة حول العالم.

من المهمّ أن نعرف أنّ طلبات HTTP هي طلبات مستقلّة بذاتها وعديمة الحالة (stateless)، بمعنى أن كلّ طلب يصدر من المتصفّح للخادوم نفسه لا يعرف أيّ شيء عن الطّلبات السّابقة أو اللاحقة، وكذلك الخادوم؛ إلا إذا تمّ إرفاق معرّف مُميّز (session ID) يتّفق عليه الطّرفان وينتقل مع كلّ طلب بين الجهتين. بدون الجلسات كنت ستحتاج إلى إدخال اسم مستخدمك وكلمة المرور في كلّ مرة تنتقل فيها إلى صفحة جديدة على فيس بوك!

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

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

لنلخّص الأمر: تسمح الجلسات بربط طلبات HTTP المتتالية بحيث يتعرّف الخادوم على كونها صادرة من جهة واحدة، مما يسمح له بتخصيص الإجابة على هذه السلسلة من الطّلبات دون غيرها، سنستفيد من هذا في حفظ تسجيل الدّخول المستخدم بحيث لا نطلب منه كلمة المرور عندما ينتقل من صفحة لأخرى. لنعد الآن إلى شيفرة تسجيل الدّخول ولنفكّر، ما الذي نحتاجه لحفظ الجلسة؟

عندما يُسجّل المستخدم دخوله للمرّة الأولى، نحتاج إلى حفظ مُعرّف الجلسة على الخادوم ليكون بإمكاننا مقارنته مع الطّلبات التّالية، وهذا يعني أنّنا بحاجة لوسيلة لحفظ معرّف الجلسة لكل مستخدم. في Express يتوفّر البرنامج الوسيط express-session الذي يتولّى هذه المهمّة كاملةً. قم بتثبيت هذه الوحدة، ثم لنقم باستيرادها:

var express = require('express');
// ...

var session = require('express-session');

نريد استخدام express-session على مستوى التّطبيق بالكامل، ما يعني أنّنا نريد لها أن تتعقّب كلّ الطّلبات على جميع الرّوابط المسجّلة مما يسمح بمتابعة الجلسة، تسمّى هذه الوحدات البرامج الوسيطة على مستوى التّطبيق (Application-level middleware) بعكس الأسلوب الّذي استخدمناه في body-parser لتفسير متن الطّلب في نماذج إنشاء المستخدم وتسجيل الدّخول (برامج وسيطة على مستوى المُوجّه Router-level middleware). يمكن للوحدة أن تُستعمل بالأسلوبين.

تستخدم الوحدات على مستوى التّطبيق باستدعاء الوظيفة use()‎ لتطبيقنا:

app.use(session({
    secret: "my top secret",
    resave: false,
    saveUninitialized: true
}));

تستقبل وحدة session كائن الإعدادات الذي يتضمّن:

  • secret: كلمة سرّيّة تسمح بتجزئة معرّف الجلسة لحمايته، ضع ما تشاء هنا.
  • resave: هل يجب كتابة الجلسة مع كلّ طلب حتّى وإن لم تتغيّر؟
  • saveUninitialized: هل يجب حفظ الجلسات الجديدة تلقائيًّا إلى الخادوم؟

لا تقلق إن كانت هذه الإعدادات غامضة، ستتعرّف على فائدتها مع مرور الوقت.

تذكّر أنّ ترتيب استدعاءات البرامج الوسيطة مهمّ، يجب أن نضيف الشّيفرة السّابقة قبل تسجيل الروابط لنتمكّن من متابعة الجلسة عبر كلّ الرّوابط المسجّلة.

حسنًا من المفترض الآن أن يقوم المتصفّح بحفظ معرّف الجلسة ثم نقله مع كلّ طلب، لنتحقّق من هذا؛ شغّل البرنامج ثم زر الصفحة الرئيسية للمدوّنة، افتح أدوات المطوّرين (Ctrl + Shift + K في فيرفكس)، ثم انتقل إلى تبويب الشّبكة واضغط زر إعادة تحميل الصّفحة، سيبدأ المتصفّح بمراقبة الطّلبات، سيظهر طلب للصفحة الرئيسيّة مع بداية تحميلها، انقر عليه وابحث عن ترويسة Cookie في الجانب، لاحظ أنّها تحوي قيمة connect.sid، وهذا هو المعرّف المميّز الذي سينتقل بين الطّلبات، للتأكد من ذلك انتقل إلى صفحة أخرى مثل ‎/posts/hello-world وكرّر العمليّة، ستجد أن معرّف الجلسة ثابت لا يتغيّر.

devtools-cookie-header.thumb.jpg.d957202

عظيم! أصبح بإمكاننا الآن تمييز الطّلبات ومتابعتها، لكن ما الفائدة التي جنيناها إلى الآن! في الحقيقة لا شيء، نحتاج إلى الاستفادة من كون معرّف الجلسة مميّزًا بحيث نعلم أن المستخدم الذي تحمل طلباته هذا المعرّف قد سجّل دخوله فلا نطلب منه كلمة المرور في كلّ طلب، وسنستفيد من ذلك أيضًا في تخصيص محتوى الصّفحة وإتاحة وصول الكُتَّاب إلى صفحة إنشاء التّدوينات فيما بعد.

نحتاج إلى حفظ معرّف الجلسة في جدول بقاعدة البيانات لنتمكّن من طلبه لاحقًا ومقارنته مع الطّلبات القادمة، لنُنشئ جدولاً يحفظ معرّفات الجلسات لكلّ مستخدم:

CREATE TABLE `sessions` (session_id VARCHAR(100) NOT NULL PRIMARY KEY, username VARCHAR(50) NOT NULL, FOREIGN KEY (username) REFERENCES `users` (username), INDEX (username));

من المهمّ أن نفهم أنّ معرّف الجلسة يحلّ محلّ كلمة المرور واسم المستخدم معًا، لهذا من الضّروري أن يكون مميّزًا (PRIMARY KEY) بحيث لا يتطابق معرّفان لمستخدمين مختلفين، من المهمّ، للسبب ذاته، حماية الجلسة وتوليدها بطريقة عشوائية، وهذا هو سبب استخدامنا للكلمة السّريّة secret في إعدادات express-session، من إجراءات الأمان الشّائعة إضافة مهلة تنتهي بعدها صلاحيّة الجلسة، وهذا هو السّبب الذي تطالبك لأجله بعض المواقع بإعادة تسجيل دخولك بعد فترة من الزّمن؛ لكنّنا لن نُشغل بالنا بهذه التّفاصيل الآن.

حسنًا لدينا الآن معرّف الجلسة وجدول لحفظه، كل ما نحتاجه عند تسجيل الدّخول بشكل صحيح حفظ معرّف الجلسة إلى الجدول، لنعد إذًا إلى شفرة تسجيل الدّخول الّتي تركناها في الفقرة السّابقة:

/*
...
*/

var cookieParser = require("cookie-parser");
app.use(cookieParser());

/*
...
*/

app.post("/sessions", parseBody, function(request, response, next) {
    var username = request.body.username;
    var password = request.body.password;

    if (!username || !password) {
        response.status(400);
        response.send("يجب توفير اسم المستخدم وكلمة المرور");
        return;
    }

    connection.query("SELECT username, password FROM `users` WHERE username=?", [ username ], function(err, rows) {
        var user = rows[0];
        if (!user) {
            response.status(400);
            response.send("لا يوجد مستخدم يطابق اسمه اسم المستخدم المطلوب");
            return;
        }

        bcrypt.compare(password, user.password, function(err, result) {
            if (err) {
                response.status(500);
                response.send("وقع خطأ من جهة الخادم، حاول تسجيل الدخول لاحقًا");
                return;
            }

            if (result == true) {

                connection.query("INSERT INTO `sessions` (session_id, username) VALUES (?, ?)", [ request.cookies["connect.sid"], username ], function(err) {
                    if (err) return next(err); // تعامل مع الخطأ
                    response.status(200);
                    response.send("تم تسجيل الدّخول");
                })

            } else {
                response.status(401);
                response.send("كلمة المرور التي أرسلتها خاطئة");
            }

        })
    });

})

توفّر لنا express-session معرّف الجلسة ضمن الكعكة تحت الاسم connect.sid والذي يمكن تغييره بضبط القيمة name في إعدادات الوحدة. استخدمنا الوحدة cookie-parser التي تقوم بما يوحي به اسمها وتوفّر لنا الكعكات ضمن كائن الطّلب للحصول على معرّف الجلسة.

نكاد ننتهي من إنشاء نظام المستخدمين، بقي علينا فقط إرفاق معلومات المستخدم مع كلّ دالّة توجيه لنتمكّن من عرض اسم المستخدم في الصّفحة وإتاحة تسجيل الخروج، بل يمكننا أيضًا توجيهه إلى صفحات خاصّة به أو منعه من الوصول إلى صفحات أخرى، سنقوم بإضافة دالّة توجيه تسبق جميع روابطنا وتقوم بإرفاق بيانات المستخدم (بعد جلبها من قاعدة البيانات) وإضافتها إلى كائن الطّلب:

app.use(function(request, response, next) {
    var session_id = request.cookies["connect.sid"];
    if (session_id) {
        connection.query("SELECT users.id, users.username, full_name, is_author FROM `users` JOIN `sessions` ON users.username=sessions.username WHERE session_id=?", [ session_id ], function(err, rows) {
            if (!err && rows[0]) {
                request.user = rows[0];
            }

            next();
        })
    } else {
        next();
    }
})

في الحقيقة، ما كتبناه للتوّ ليس سوى برنامج وسيط، لا يختلف في شيء عن البرامج الوسيطة التي استعملناها مثل express-session عدا أنّ الأخيرة يوفّرها مطوّرون آخرون كحزمة على npm يمكن استيرادها.

في دوال التّوجيه التّالية، سيتوفّر لدينا كائن request.user يتضمّن معلومات المستخدم الحالي، لنجرّب ذلك بإنشاء صفحة الملفّ الشّخصي للمستخدم (views/profile.jade):

doctype html
html(lang="ar", dir="rtl")
    head
        title الملف الشخصي
    body
        style
            :css
                body {
                    font-family: Arial, sans-serif;
                }

        if (user)
            h1 #{ user.full_name } (#{ user.username })
            hr
        else
            p لم تقم بتسجيل دخولك

app.get("/profile", function(request, response) {
    response.render("profile", { user: request.user })
})

سنقوم بتوفير الكائن request.user للقالب، والذي سيكون غير معرّف إن لم يُوجد في قاعدة البيانات أو إن لم يسجل المستخدم دخوله، سيتولى القالب هذه الحالة ويعرض الرسالة المناسبة. لاحظ دعم Jade للجمل الشّرطيّة.

حسنًا، لنجرّب الآن ما كتبناه، شغّل البرنامج ثم زر الصفحة http://localhost:3000/login، سجّل الدّخول باسم المستخدم admin وكلمة المرور 123456، من المفترض أن تنتقل إلى صفحة تخبرك بنجاح العملية، الآن انتقل إلى http://localhost:3000/profile لتشاهد الملف الشّخصيّ (حسنًا لا يبدو عظيمًا جدًّا، لكنّنا سنحسّنه لاحقًا):

profile-signed-in.thumb.jpg.e0c853564bb9

تهانينا! لقد أنشأنا نظامًا للمستخدمين وأصبح بإمكاننا عرض محتوى مخصّص لكلّ مستخدم! في الجزء القادم سنتيح لأنفسنا كتابة التّدوينات، وللمستخدمين إضافة التعليقات، وستكون أعظم مدوّنة في التّاريخ!


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...