Menu
CoddyTech

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

جزء من قسم المصادقة والأمان في رحلة Node.js على Coddy. الدرس 10 من 24.

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

يجب أن يكون المعرّف عشوائيًا. ملف تعريف ارتباط يحتوي على اسم المستخدم يتيح لأي شخص كتابة Cookie: sid=ada وانتحال شخصية Ada. تمنح randomBytes(24) 24 بايتًا عشوائيًا، أي 192 بتًا، لا يستطيع أحد تخمينها. تكتبها toString('base64url') على هيئة 32 حرفًا ورقمًا، و- و_، وهي أحرف آمنة للاستخدام في ملف تعريف الارتباط. اضبط ملف تعريف الارتباط على httpOnly حتى لا تتمكن البرامج النصية في الصفحة من قراءته.

const cartId = randomBytes(24).toString('base64url');
await carts.insertOne({ _id: cartId, items: [] });
res.cookie('cart', cartId, { httpOnly: true });

// في طلب لاحق
const cart = await carts.findOne({ _id: parseCookies(req.get('cookie')).cart });
challenge icon

التحدي

سهل

تفتح هذه الخطوة مشروع واجهة API للملاحظات الخاصة المُعدّ مسبقًا للدورة، بدلًا من ملفات مشروعك من درس التذكّر باستخدام ملفات تعريف الارتباط (بما فيها server.js). يبقى عملك السابق في درسه الخاص. اقرأ الملفات المزوّدة قبل تعديل server.js. تحتوي هذه النسخة على مسارات التسجيل وتسجيل الدخول للملاحظات الخاصة.

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

  1. في server.js، استورد randomBytes من node:crypto. استورد sessions من ./db.js إلى جانب notes وusers. يحتوي db.js المقفل بالفعل على مجموعة sessions.
  2. في POST /login، بعد التحقق من كلمة المرور، أنشئ معرّف جلسة: 24 بايتًا عشوائيًا مكتوبًا كنص بترميز base64url. أي 32 حرفًا من الأحرف والأرقام و- و_، مثل oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN. أنشئ معرّفًا جديدًا عند كل تسجيل دخول.
  3. احفظ الجلسة في sessions كمستند ذي حقلين:
    • _id: معرّف الجلسة.
    • userId: قيمة _id الخاصة بالمستخدم الذي سجّل الدخول.
  4. أرسل المعرّف إلى المتصفح في ملف تعريف ارتباط اسمه sid، مع الخيار httpOnly، لكيلا تتمكن النصوص البرمجية في الصفحة من قراءته. يظل رد تسجيل الدخول {"username": "ada"}.
  5. أضف GET /me. يقرأ ملف تعريف الارتباط sid، ويعثر على الجلسة التي تحمل قيمة _id هذه، ثم يعثر على المستخدم الخاص بتلك الجلسة. ويُعيد الرد {"username": "ada"}. عندما لا يوجد ملف تعريف ارتباط sid، أو لا توجد جلسة بهذا المعرّف، يعيد الرد الحالة 401 و{"error": "Log in first"}. يجب أن يحصل أي شخص يكتب ملف تعريف ارتباط يدويًا، مثل sid=ada، على الحالة 401.

جرّب ذلك: اضغط «العب». أرسل POST /signup مع {"username": "ada", "password": "correct horse"}. أرسل GET /me: الحالة 401 و{"error": "Log in first"}. أرسل POST /login مع النص نفسه المستخدم للتسجيل: الحالة 200. تعرض علامة التبويب Headers في الرد Set-Cookie: sid=...; Path=/; HttpOnly، مع ظهور معرّفك العشوائي بدلًا من النقاط. أرسل GET /me مرة أخرى. يرسل المتصفح ملف تعريف الارتباط مجددًا، لذا ستحصل على {"username": "ada"}. افتح Data: تحتوي sessions على مستند واحد، مع معرّفك في _id ومعرّف ada في userId.

جرّب بنفسك

import express from 'express';
import { parseCookies } from './cookies.js';
import { notes, users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';

const app = express();
app.use(express.json());

app.post('/signup', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const password = String(req.body.password ?? '');
    if (!username || password.length < 8) {
        return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
    }
    if (await users.findOne({ username })) {
        return res.status(409).json({ error: 'That username is taken' });
    }
    await users.insertOne({ username, passwordHash: await hashPassword(password) });
    res.status(201).json({ username });
});

app.post('/login', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const user = await users.findOne({ username });
    if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
        return res.status(401).json({ error: 'Wrong username or password' });
    }
    res.json({ username: user.username });
});

// يحسب عدد المرات التي طلب فيها هذا المتصفح GET /visit.
app.get('/visit', (req, res) => {
    const cookies = parseCookies(req.get('cookie'));
    const visits = Number(cookies.visits ?? 0) + 1;
    res.cookie('visits', String(visits));
    res.json({ visits });
});

// الملاحظات. يمكن لأي شخص قراءتها والكتابة فيها حاليًا.
app.get('/notes', async (req, res) => {
    res.json(await notes.find().toArray());
});

app.post('/notes', async (req, res) => {
    const note = { text: String(req.body.text ?? '') };
    await notes.insertOne(note);
    res.status(201).json(note);
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});

جميع دروس المصادقة والأمان

تدرّب بنفسك: Playground لـ Express