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

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


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

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

إنشاء صفحة "حساب جديد" و"تسجيل الدّخول"

نعلم إذًا أنّنا بحاجة أولاً إلى آلية لإنشاء الحسابات على خادومنا، وأوّل ما نقوم به إنشاء صفحة على الرّابط ‎/signup تحوي نموذجًا يعبّئه المستخدم:

doctype html
html(lang="ar", dir="rtl")
    head
        title إنشاء مستخدم جديد
    body
        style
            :css
                body {
                    font-family: Arial, sans-serif;
                }

        h1 مُدوّنتي
        hr
        h2 إنشاء مستخدم جديد
        form(action="/accounts", method="POST")
            label(for="name") اسمك: 
            input(type="text", name="name", placeholder="الاسم كاملًا", required)
            br
            label(for="name") كلمة المرور: 
            input(type="password", name="password", placeholder="اختر كلمة مرور قويّة", required)
            br
            label(for="username") اسم المستخدم: 
            input(type="text", name="username", placeholder="حروف لاتينية، 50 حرفًا على الأكثر", required)
            br
            input(type="submit", value="أنشئ الحساب")

احفظ النصّ السّابق في ملف signup.jade في مجلّد views؛ ثمّ أضف النّص التّالي إلى index.js قبل آخر سطر:

app.get("/signup", function(request, response) {
    response.render("signup");
})

زر الصّفحة http://localhost:3000/signup بعد تشغيل البرنامج وحاول إنشاء مستخدم جديد، سيرسلك Express إلى صفحة تفيد بعد إمكانية تنفيذ الفعل POST على الرّابط ‎/accounts، وهو الرابط الذي اخترناه لتلقّي نماذج إنشاء المستخدمين بالنّموذج الّذي أنشأناه (لاحظ الخاصّتين action وmethod للنّموذج ضمن قالب Jade)، كلّ ما علينا الآن هو تسجيل دالّة تتعامل مع هذا الرّابط:

app.post("/accounts", function(request, response) {
    // أنشئ الحساب
})

إن كنت تتساءل لم استخدمنا POST بدلاً من GET، فالإجابة هي أنّ POST يستخدم للطّلب من الخادوم "إنشاء" الأشياء الجديدة (بينما يطلب GET "الحصول" عليها)، هذا أوّلًا، ثانيًا فإنّ إرسال الطّلب باستخدام GET، وعلى الرّغم من أنّه ممكن، إلّا أنّه قد يكشف كلمة المرور الّتي اختارها المستخدم، لأنّ محتويات النّموذج (بما فيها كلمة المرور) ستُرمّز ضمن الرّابط (URL-encoded)، وكلّ المتصفّحات تحتفظ بنسخة من سجلّ تصفّح المستخدم، وهذا قد يجعلها عرضة لأن يراها الآخرون. هذا مثال عن كيفية ترميز النّماذج في طلبات GET:

http://myblog.com/signup?username=fwz&password=123456&full_name=فواز

كما ترى، ليس هذا أفضل ما يمكننا فعله لإخفاء كلمة المرور!

يرسل المتصفّح محتويات النّموذج بالفعل POST كمتن الطّلب، وعندما يتلقّاه الخادوم فإنّنا بحاجة إلى تحويله من نصّ مجرّد إلى صيغة كائن JavaScript، لا يقدّم Express هذه الإمكانيّة وحده، ولكنّه يوفّر وحدة منفصلة تُدعى body-parser‏ للقيام بهذه المهمّة، قد يبدو هذا غريبًا للقادمين من PHP، لكنّها الطّريقة الّتي تسير بها الأمور في Node.js، ولهذا فوائده إذ يمكنك استبدال وحدة بوحدة أخرى تؤدّي الوظيفة ذاتها لكن قد تكون أسرع أو تقدّم وظائف أكثر، وكذلك يسمح هذا النّهج بتطوير الوحدات الصّغيرة بشكل أسرع دون الانتظار إلى صدور نسخة جديدة من إطار العمل كاملاً.

كاختبار لك، قم بتثبيت body-parser وحفظه في متطلّبات المشروع.

هل تذكر عندما تحدّثنا عن البرامج الوسيطة (middleware)؟ حسنًا، وحدة body-parser ليست سوى واحدة من هذه البرامج، ويأتي الاسم من كونها تتوسّط وظيفة Express لتوسّع خياراته بشكل منسجم مع سير توجيه الرّوابط. لإخبار Express برغبتنا باستخدام body-parser، علينا استيرادها ثمّ إدخالها كوسيط لعمليّة توجيه الرّابط /accounts:

var express = require("express");
var bodyParser = require("body-parser");

/*
...
*/

var parseBody = bodyParser.urlencoded({ extended: true });

app.post("/accounts", parseBody, function(request, response) {
    console.log(request.body);
})

app.listen(3000);

هذه إحدى الطّرق لاستخدام البرامج الوسيطة على أحد الرّوابط، يمكن إدخال أي عدد من البرامج الوسيطة وسينفّذها Express واحدًا تلو الآخر حتّى يصل أخيرًا إلى دالّتنا الّتي تتعامل مع الرّابط. يمكننا أيضًا استخدام body-parser وأي برنامج وسيط آخر ليتدخّل في سير التّطبيق كاملاً (ليس على رابط واحد فقط)، وسنرى كيفيّة ذلك في وقتٍ لاحق.

إن كانت صياغة السّطر var parseBody...‎ غامضة فراجع صفحة توثيق وحدة body-parser‏، الأمر يتعلّق بأسلوب المطوّر الّذي أنشأ الوحدة، قد يكون أسلوب الوحدات الأخرى مختلفًا لكنّ ما يهمّك هو أن تتعلّم كيفيّة استخدام البرامج الوسيطة.

في دالّة التّوجيه الأخيرة، سنقوم مبدئيًا بتسجيل محتويات النّموذج إلى الطّرفيّة الّتي تشغّل برنامجنا، يتوفّر الكائن request.body فقط لأنّنا استخدمنا body-parser قبل دالّتنا، والذي أتاح محتويات النّموذج في عنصر الطّلب. أعد تشغيل البرنامج وزُر الصّفحة ثم املأ الحقول واضغط "أنشئ الحساب"، عُد للطّرفيّة لتشاهد محتويات النّموذج وقد وصلت للخادوم:

body-parser-console-log.thumb.jpg.9f2c5b

حسنًا لقد وصلَنا النّموذج وهو جاهز لإدخاله في قاعدة البيانات، لكن ليس قبل التّحقّق من محتوياته. القاعدة الرئيسيّة في حماية قواعد البيانات: لا تثق بما يُدخله المستخدم! تحقّق من سلامة كلّ حقل في النّموذج قبل إدخاله، ماذا لو أرسل المستخدم حقلاً إضافيًا is_author وجعل قيمته true، سيكون بإمكانه حينئذٍ كتابة التّدوينات دون أن نسمح له بذلك!

app.post("/accounts", parseBody, function(request, response) {
    var username = request.body.username;
    var password = request.body.password;
    var full_name = request.body.name;

    if (!username || !password || username.length > 50) {
        response.status(400);
        response.send("تعذّر إنشاء الحساب، تحقّق من سلامة المُدخلات وأعد المحاولة");
        return;
    }

    connection.query("INSERT INTO `users` (username, password, full_name) VALUES (?, ?, ?)", [username, password, full_name], function(err) {
        if (err) {
            response.status(500);
            response.send("وقع خطأ أثناء إنشاء الحساب، أعد المحاولة");
            return;
        }

        response.status(201);
        response.send("أُنشئ الحساب، يمكنك الآن إنشاء المستخدم");
    });
})

من المّهمّ ألا تأخذ الكائن response.body كاملاً وتلقيه مباشرة في قاعدة بياناتك، فقد يحتوي على حقول إضافية مثل is_author. قمنا بإجراء تحقّق بسيط من طول اسم المستخدم. تتوفّر في Node.js وحدات تُعطينا إمكانيّات أوسع للتحقّق من المدخلات بحسب أنواعها (نصّيّة، أرقام، عناوين البريد... إلخ)، سنستعرض إحداها لاحقًا.

استخدمنا الرّمز 400 في حال الخطأ ومعناها Bad Request، تستخدم الأرقام ضمن 400-499 للإشارة إلى خطأ من جهة مُرسل الطّلب (خلافًا للفئة 5xx‏ الّتي تعني أنّ الخطأ من جهة الخادوم). أمّا الرّمز 201 فيعني Created (أُنشئ).

حماية كلمة المرور بدوال التّجزئة (Password hashing functions)

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

هي دالّة تجزئة غير قابلة للعكس عمليًّا، بمعنى أنّه من غير الممكن استعادة البيانات المُدخلة من قيمة التّجزئة وحدها.

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

خوارزمية التّجزئة SHA-1

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

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

علينا أنّ نفرّق التّجزئة عن التعمية (encryption) والّتي هي تحويل نصّ مجرّد (plaintext) إلى نصّ مُشفّر (ciphertext) وفق عمليّة رياضيّة قابلة للعكس، بينما تهدف التّجزئة إلى تحويل البيانات المختلفة الحجم إلى قيمة ثابتة الطّول باتّجاه واحد فقط (one-way).

في المثال السّابق أدخلنا كلمة المرور في قاعدة البيانات دون تجزئة، وهذا خطأ فادح لأنّه يسمح لمن يستطيع الوصول إلى جدول المستخدمين بالاطّلاع على كلمات مرورهم جميعًا، لعلّك تستخدم md5 أو sha1 في PHP لتجزئة كلمة المرور بشكل تقليدي، تتوفّر وحدات Node.js تسمح بتجزئة النّصوص بهذه الخوارزميات، لكنّنا سنستخدم خوارزميّة bcrypt الّتي تُعد أكثر أمانًا بمراحل من الخوارزميّتين سابقتي الذّكر:

npm install bcrypt --save

ملاحظة: تحتاج الوحدة bcrypt إلى إصدار متوافق من Python مثبّتًا على جهازك، راجع صفحة الوحدة على GitHub‏ لمزيد من التّفاصيل.

var bcrypt = require("bcrypt");

/*
...
*/

app.post("/accounts", parseBody, function(request, response) {
    var username = request.body.username;
    var password = request.body.password;
    var full_name = request.body.name;

    if (!username || !password || username.length > 50) {
        response.status(400);
        response.send("تعذّر إنشاء الحساب، تحقّق من سلامة المُدخلات وأعد المحاولة");
        return;
    }

    bcrypt.hash(password, 8, function(err, hash) {
        if (err) {
            response.status(500);
            response.send("تعذّر إنشاء الحساب، تحقّق من سلامة المُدخلات وأعد المحاولة");
            return;
        }

        connection.query("INSERT INTO `users` (username, password, full_name) VALUES (?, ?, ?)", [username, hash, full_name], function(err) {
            if (err) {
                response.status(500);
                response.send("وقع خطأ أثناء إنشاء الحساب، أعد المحاولة");
                return;
            }

            response.send(201);
            response.send("أُنشئ الحساب، يمكنك الآن تسجيل الدخول");
        });
    });
})

// ...

لنجرّب إنشاء مستخدم جديد الآن، شغّل البرنامج ثم انتقل إلى http://localhost:3000/signup، املأ الحقول بمُدخلات سليمة ثمّ اضغط "أنشئ الحساب":

signup-account-created.jpg.2b4a15aa637ef

لنتأكد من وجود الحساب في قاعدة البيانات، افتح صدفة MySQL ونفّذ الاستعلام التّالي بعد الاتّصال بقاعدة البيانات:

SELECT FROM `users` WHERE username="muhammad";

بدّل اسم المستخدم بالاسم الذي ملأته في حقل "اسم المستخدم" عند إنشاء الحساب، ستحصل على نتيجة مشابهة لهذه:

+----+----------+--------------------------------------------------------------+-----------+-----------+‎
| id | username | password                                                     | full_name | is_author |
+----+----------+--------------------------------------------------------------+-----------+-----------+
|  2 | muhammad | $2a$08$6GFnpkKY6VQuB6/y4NCrg.AK9jI25XyfS6APz4rP8w1bpICKNR79G | محمد‎      |         0 |
+----+----------+--------------------------------------------------------------+-----------+-----------+
1 row in set (0.00 sec)

لاحظ كون كلمة المرور مُجزّئة ممّا يجعل معرفتها مستحيلة لمن يصل لجدول المستخدمين.

تسجيل الدّخول

لُننشئ صفحة تسجيل الدّخول على الرابط ‎/login مع القالب views/login.jade:

doctype html
html(lang="ar", dir="rtl")
    head
        title تسجيل الدخول
    body
        style
            :css
                body {
                    font-family: Arial, sans-serif;
                }

        h1 مُدوّنتي
        hr
        h2 تسجيل الدخول
        form(action="/sessions", method="POST")
            label(for="username") اسم المستخدم: 
            input(type="text", name="username", required)
            br
            label(for="name") كلمة المرور: 
            input(type="password", name="password" required)
            br
            input(type="submit", value="سجّل الدخول")

سنضيف هذه الشيفرة للتّعامل مع تسجيل الدّخول:

app.get("/login", function(request, response) {
    response.render("login");
})

app.post("/sessions", parseBody, function(request, response) {
    // ابحث عن المستخدم وتأكد من صحة كلمة المرور
})

// ...

الخطوة الأولى في تسجيل الدّخول تتضمّن التّحقّق من وجود اسم المستخدم ومقارنة كلمة المرور بعد تجزئتها (hashing) للكلمة المُجزئة في قاعدة البيانات.

app.post("/sessions", parseBody, function(request, response) {
    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) {
                // كلمتا المرور متطابقتان

                response.status(200);
                // احفظ الجلسة على المتصفّح
            } else {
                response.status(401);
                response.send("كلمة المرور التي أرسلتها خاطئة");
            }

        })
    });

})

في البداية نبحث في قاعدة البيانات عن سطر يوافق حقل username فيه القيمة username الّتي أرسلها المتصفّح، إن وُجد هذا المستخدم فإنّنا نستخدم الوظيفة compare()‎ الّتي توفّرها bcrypt لمقارنة كلمة المرور المُجزّئة مع كلمة المرور الّتي أرسلها المستخدم، إن كانت نتيجة المعامل result مساوية لـtrue، فهذا يعني أنّ كلمة المرور صحيحة. وإلّا فإنّنا نُرسل الرّمز 401 ويعني Unauthorized (غير مُصرّح له) مع رسالة مناسبة للدّلالة على فشل تسجيل الدّخول.

لم ننتهِ بعد من تسجيل الدّخول، لكنّنا سنؤجّل الخطوة الثانية قليلاً، لأنّها تعتمد على فهمنا للجلسات (sessions)، الّتي ستكون موضوع الدّرس القادم.


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...