تأمين تطبيقات Node.js: دليل شامل لمصادقة المستخدمين باستخدام الكوكيز والجلسات و Passport.js

دقائق القراءة: 10

تُعد مصادقة المستخدمين (User Authentication) حجر الزاوية في بناء أي تطبيق ويب حديث، سواء كان منصة تواصل اجتماعي، نظامًا تعليميًا، أو أي خدمة تتطلب تحديد هوية المستخدمين. إنها الخطوة الأولى نحو توفير تجربة شخصية وآمنة للمستخدمين، وتُمكن التطبيق من التمييز بين المستخدمين المصادق عليهم وغير المصادق عليهم، وبالتالي التحكم في الوصول إلى الموارد والبيانات الحساسة.

في هذا الدليل الشامل، سنتعمق في استكشاف المفاهيم الأساسية لمصادقة المستخدمين ضمن بيئة Node.js، بدءًا من الآليات التقليدية وصولًا إلى الحلول الأكثر تطورًا. سنركز على ثلاث طرق رئيسية: الكوكيز (Cookies)، الجلسات (Sessions)، ومكتبة Passport.js القوية، مع تقديم أمثلة عملية وشرح مبسط لمساعدتك على فهم هذه التقنيات وتطبيقها بفعالية.

مصادقة المستخدمين باستخدام الكوكيز (Cookies)

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

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

وفقًا لمواصفات w3.org، تُعرّف الكوكيز بأنها: "أجزاء نصية تُخزّن على جهاز العميل وتُرسل مع طلب HTTP إلى موقع الويب الذي أُنشئت من أجله." تُنشأ الكوكيز وتُخزّن بعد تسجيل دخول المستخدم، ويُستشار بها قبل تلبية الطلبات المتتالية، ثم تنتهي صلاحيتها وفقًا لمدة زمنية محددة.

إعداد تطبيق Express مع cookie-parser

نبدأ بإعداد تطبيق Express الخاص بنا وتضمين وسيط cookie-parser. هذا الوسيط يقوم بتحليل رأس الكوكيز (Cookie header) من الطلب، ويضيف الكوكيز المحللة إلى الكائن req.cookies أو req.signedCookies (إذا كانت المفاتيح السرية مستخدمة) لمعالجتها لاحقًا. يأخذ cookie-parser مفتاحًا سريًا (secret key) كوسيط، والذي يُستخدم لإنشاء توقيع HMAC لقيمة الكوكي الحالية. إذا تم التلاعب بالقيمة لاحقًا، فسيتم اكتشاف ذلك لأن التوقيع الذي تم إنشاؤه وقت الإنشاء لن يتطابق مع التوقيع الحالي.

 let express = require('express');
 let cookie_parser = require('cookie-parser');
 let app = express();
 app.use(cookie_parser('1234'));

آلية المصادقة الأولية وتعيين الكوكيز

عندما يزور المستخدم عنوان URL المناسب (مثل /login أو ما شابه)، نحتاج إلى إجراء بعض التحققات. لنفترض أن المستخدم يسجل الدخول لأول مرة. في هذه الحالة، لن يكون هناك كوكي موقّع (signed cookie) مناسب للاستخدام.

نستخدم رأس الاستجابة WWW-Authenticate لتحديد طريقة المصادقة التي يجب استخدامها للوصول إلى مورد (في مثالنا، طريقة "Basic"). تتكون الاستجابة من العميل من اسم المستخدم وكلمة المرور مفصولين بنقطتين رأسيتين (:)، ويتم ترميزها بـ base64 وإرفاقها برأس Authorization في الطلب. يُطلب من المستخدم معلومات المصادقة، والتي يتم استخلاصها والتحقق منها. في التطبيقات الحقيقية، يجب التحقق من قاعدة بيانات، لكننا سنقوم بتحقق بسيط (naïve check) للتبسيط في الوقت الحالي. إذا تم إعطاء القيم الصحيحة، نقوم بتعيين كوكي مناسب. وإلا، نطلب من المستخدم المصادقة مرة أخرى.

 app.use('/', (req, res, next) => {
    let cookie_Stuff = req.signedCookies.user; // التحقق مما إذا كان الكوكي الموقّع موجودًا

    if (!cookie_Stuff) { // صحيح في حالتنا لأن المستخدم يسجل الدخول لأول مرة
        let auth_Stuff = req.headers.authorization;
        if (!auth_Stuff) { // لم يتم تقديم معلومات مصادقة
            res.setHeader("WWW-Authenticate", "Basic");
            res.sendStatus(401); // إرسال رمز حالة 401 (غير مصرح به)
        } else {
            // استخلاص اسم المستخدم وكلمة المرور من الترميز
            let step1 = Buffer.from(auth_Stuff.split(" ")[1], 'base64');
            let step2 = step1.toString().split(":"); // استخلاص اسم المستخدم وكلمة المرور في مصفوفة

            if (step2[0] == 'admin' && step2[1] == 'admin') {
                console.log("WELCOME ADMIN");
                // تخزين كوكي باسم 'user' وقيمة 'admin' مع التوقيع
                res.cookie('user', 'admin', { signed: true });
                res.send("Signed in the first time");
            } else {
                // معلومات مصادقة خاطئة، إعادة المحاولة
                res.setHeader("WWW-Authenticate", "Basic");
                res.sendStatus(401);
            }
        }
    } else { // الكوكي الموقّع مخزن بالفعل
        if (req.signedCookies.user == 'admin') {
            res.send("HELLO GENUINE USER");
        } else {
            // معلومات خاطئة، يُطلب من المستخدم المصادقة مرة أخرى
            res.setHeader("WWW-Authenticate", "Basic");
            res.sendStatus(401);
        }
    }
});

الآن، أنت تعرف كيفية مصادقة المستخدم باستخدام الكوكيز! يمكنك التحقق من الكوكي المخزن بالانتقال إلى قسم التخزين (Storage) في أدوات المطور بالمتصفح الخاص بك والذهاب إلى علامة تبويب الكوكيز (Cookies). ستظهر قيمة الكوكي والقيم المحللة بشكل منفصل في قسمين (في متصفح فايرفوكس، على سبيل المثال).

مصادقة المستخدمين باستخدام الجلسات (Sessions)

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

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

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

إعداد الجلسات باستخدام express-session

تُنشئ مكتبة express-session وسيط جلسة (session middleware) يتيح لك إعداد الجلسات والتعامل معها بسهولة. التخزين الافتراضي من جانب الخادم هو MemoryStore. لتخزين معلومات الجلسة كملفات JSON، تحتاج إلى مكتبة session-file-store.

يقوم الكود التالي بما يلي:

  • يقوم بإعداد تطبيق Express.
  • يُخبر الوسيط بطلب المصادقة إذا لم يتم تحديدها، وإلا يتحقق مما إذا كان اسم المستخدم وكلمة المرور متطابقين.
  • إذا لم يتطابقا، فإنه يحتاج مرة أخرى إلى تقديم نفس الطلب للمصادقة، وإلا يتم إنشاء الجلسة.
  • ثم يضيف اسم المستخدم كخاصية user ويتحقق منها لاحقًا.

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

 let app = express();
 let session = require('express-session');
 let File_Store = require('session-file-store')(session);

 app.use(session({
    store: new File_Store(),
    secret: 'hello world',
    resave: true,
    saveUninitialized: true
 }));

 app.use('/', (req, res, next) => {
    if (!req.session.user) {
        console.log("Session not set-up yet");
        if (!req.headers.authorization) {
            console.log("No auth headers");
            res.setHeader("WWW-Authenticate", "Basic");
            res.sendStatus(401);
        } else {
            let auth_stuff = Buffer.from(req.headers.authorization.split(" ")[1], 'base64');
            let step1 = auth_stuff.toString().split(":");
            console.log("Step1: ", step1);

            if (step1[0] == 'admin' && step1[1] == 'admin') {
                console.log('GENUINE USER');
                req.session.user = 'admin';
                res.send("GENUINE USER");
            } else {
                res.setHeader("WWW-Authenticate", "Basic");
                res.sendStatus(401);
            }
        }
    } else {
        res.send(`Welcome back, ${req.session.user}`); // مثال على استخدام الجلسة بعد المصادقة
    }
 });

مصادقة المستخدمين باستخدام وسيط Passport.js

بعد أن تعرفنا على كيفية مصادقة المستخدمين باستخدام الكوكيز والجلسات، حان الوقت لاستكشاف طريقة ثالثة وأكثر قوة: Passport.js. تُعد Passport.js وسيط مصادقة مرنًا وقابلاً للتوسيع لتطبيقات Node.js، يتيح لك مصادقة المستخدمين باستخدام مجموعة متنوعة من "الاستراتيجيات" (strategies)، بما في ذلك الجلسات (sessions) و OAuth، بالإضافة إلى إمكانية إنشاء استراتيجيات مخصصة.

إعداد استراتيجية المصادقة المحلية (Local Strategy)

يُعد الكود التالي جميع الوحدات النمطية المطلوبة لتحديد استراتيجية محلية مناسبة. تتيح استراتيجية passport-local المصادقة باستخدام اسم المستخدم وكلمة المرور فقط. من المهم جدًا التأكد من أن اسم عنصر إدخال النموذج (form input element) لاسم المستخدم هو "username" ولكلمة المرور هو "password". يمكنك أيضًا تغيير الأسماء الافتراضية للحقول بتمرير كائن JSON قبل دالة الاستدعاء (callback function) في استدعاء local_strategy، حيث يكون هيكل JSON كالتالي: { usernameField: "Some new name for this field", passwordField: "Some new name for this field" }.

 let passport = require('passport');
 let bcrypt = require('bcrypt-nodejs'); // تُستخدم لتشفير كلمات المرور
 let User_Obj = require('./Set_Up_Database_Stuffs'); // افتراض وجود ملف لإعداد قاعدة البيانات
 const local_strategy = require('passport-local').Strategy;

 // تهيئة Passport
 app.use(passport.initialize());
 app.use(passport.session()); // إذا كنت تستخدم الجلسات مع Passport

تعريف استراتيجية passport-local

تُظهر الأسطر التالية تطبيقًا مبسطًا لاستراتيجية local-strategy حيث يتم التحقق من البيانات من قاعدة بيانات تحتوي على حقل لاسم المستخدم. يتم استخدام bcrypt لمقارنة كلمة المرور المدخلة بكلمة المرور المشفرة المخزنة في قاعدة البيانات، مما يضمن أمانًا أفضل.

 passport.use(new local_strategy(async (username, password, done) => {
    console.log("Here inside local_strategy", username, password);
    try {
        let row1 = await User_Obj.findOne({ username: username });
        console.log(row1);
        // row1 يجب أن يكون الكائن من قاعدة البيانات حيث يتطابق حقل اسم المستخدم مع اسم المستخدم المقدم.
        if (row1 == null) {
            console.log("NO RECORDS FOUND");
            return done(null, false); // لا يوجد سجل للمستخدم
        } else {
            console.log("Record found");
            console.log(row1);
            if (bcrypt.compareSync(password, row1.password)) { // مقارنة كلمة المرور النصية مع الهاش
                console.log("The passwords match");
                console.log("Finished authenticate local");
                return done(null, row1); // المصادقة ناجحة
            } else {
                console.log("The passwords don't match");
                return done(null, false); // كلمة المرور غير متطابقة
            }
        }
    } catch (err) {
        console.log("Some error here");
        return done(err); // حدث خطأ
    }
 }));

 // تسلسل وإلغاء تسلسل المستخدم للجلسات (مهم عند استخدام الجلسات مع Passport)
 passport.serializeUser((user, done) => {
    done(null, user.id); // تخزين معرف المستخدم في الجلسة
 });

 passport.deserializeUser(async (id, done) => {
    try {
        const user = await User_Obj.findById(id); // استعادة المستخدم من قاعدة البيانات باستخدام المعرف
        done(null, user);
    } catch (err) {
        done(err, null);
    }
 });

تكامل Passport.js مع مسارات Express

عندما يصل المستخدم إلى مسار /auth، فإنه يُفعّل الاستراتيجية المحلية (local strategy) التي تُنفذ كما هو محدد. إذا كان هناك فشل أثناء المصادقة، فإنه يُعيد التوجيه إلى صفحة الفشل. وإلا، فإنه يُعيد التوجيه إلى صفحة المقالات (أو أي صفحة تحتاجها).

أما بالنسبة لطلب POST إلى /donesignup، فإنه يتحقق مما إذا كان اسم المستخدم موجودًا بالفعل. إذا لم يكن كذلك، فإنه يضيفه كسجل إلى قاعدة البيانات، حيث تكون الحقول هي اسم المستخدم وهاش (hash) كلمة المرور المعطاة.

 app.post('/auth', passport.authenticate('local', {
    successRedirect: '/articles',
    failureRedirect: '/failurepage' // يُفعّل الاستراتيجية المحلية. إذا نجحت، يُعاد التوجيه إلى صفحة المقالات وإلا تُعرض صفحة الفشل.
 }));

 // تأكد من أن objForUrlencoded هو وسيط لتحليل بيانات النموذج
 app.post('/donesignup', express.urlencoded({ extended: false }), async (req, res) => {
    console.log(req.body);
    try {
        let row1 = await User_Obj.findOne({ username: req.body.username });
        console.log(row1);
        if (row1 != null) {
            console.log("That username already exists");
            res.render('signup', { message: 'اسم المستخدم موجود بالفعل!' }); // مثال على إعادة العرض مع رسالة
        } else {
            // الحصول على هاش كلمة المرور لتخزينها في قاعدة البيانات
            let hashedPassword = bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(8), null);
            let save_this = new User_Obj({
                username: req.body.username,
                password: hashedPassword
            });
            console.log(save_this);
            await save_this.save(); // استخدام await لضمان حفظ البيانات
            console.log("SAVED IT");
            res.redirect('/login'); // إعادة التوجيه إلى صفحة تسجيل الدخول بعد التسجيل الناجح
        }
    } catch (err) {
        console.error("Error during signup:", err);
        res.status(500).send('حدث خطأ أثناء التسجيل.');
    }
 });

الخلاصة التقنية

لقد استعرضنا في هذا الدليل ثلاث طرق أساسية لمصادقة المستخدمين في تطبيقات Node.js: الكوكيز، الجلسات، ومكتبة Passport.js. كل طريقة تقدم مزايا وعيوبًا خاصة بها، وتعتمد أفضلية الاختيار على متطلبات الأمان، قابلية التوسع، وتعقيد التطبيق.

  • الكوكيز (Cookies): توفر طريقة بسيطة لتتبع المستخدمين، ولكنها تُخزّن على جانب العميل، مما يجعلها عرضة للتلاعب وتحد من حجم البيانات التي يمكن تخزينها. استخدام الكوكيز الموقّعة (signed cookies) يعزز الأمان ضد التلاعب.
  • الجلسات (Sessions): تُعد أكثر أمانًا من الكوكيز المباشرة لأن بيانات المستخدم تُخزّن على الخادم، ويُمرر فقط معرف الجلسة إلى العميل. هذا يقلل من مخاطر التلاعب بالبيانات الحساسة ويوفر مرونة أكبر في إدارة حالة المستخدم.
  • Passport.js: هي الحل الأكثر شمولية ومرونة، حيث توفر إطار عمل قويًا للمصادقة يدعم استراتيجيات متعددة (مثل المحلية، OAuth) وتُبسط عملية دمج آليات مصادقة معقدة. تُعد الخيار الأمثل للتطبيقات الكبيرة التي تتطلب مرونة وأمانًا عاليين.

في حين أن الأمثلة المقدمة هنا مبسطة لأغراض الشرح، إلا أنه من الضروري في التطبيقات الإنتاجية دائمًا استخدام قواعد البيانات لتخزين بيانات المستخدمين بشكل آمن، وتطبيق أفضل ممارسات الأمان مثل تشفير كلمات المرور باستخدام مكتبات مثل bcrypt، والتعامل مع الأخطاء بشكل صحيح. اختيار الطريقة المناسبة لمصادقة المستخدمين هو قرار حاسم يؤثر على أمان وكفاءة تطبيقك.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *