Menu
CoddyTech

علامات ملفات تعريف الارتباط

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

تنتقل ملفات تعريف الارتباط التي لا تتضمن علامات عبر HTTP غير المشفّر، وتُرفق بالطلبات التي تطلقها مواقع أخرى، وتظل موجودة حتى يُغلق المتصفح. هناك أربعة خيارات في res.cookie تزيد من حمايتها:

  • httpOnly: لا تستطيع النصوص البرمجية في الصفحة قراءتها.
  • sameSite: يمنع 'lax' إرفاقها بالطلبات التي ترسلها مواقع أخرى، إلا عندما يتبع المستخدم رابطًا؛ أما 'strict' فيمنع إرفاقها بتلك الطلبات أيضًا.
  • secure: تُرسل عبر HTTPS فقط، ولا تُرسل أبدًا عبر HTTP غير المشفّر، حيث يمكن للآخرين على الشبكة قراءتها.
  • maxAge: مدة بقائها، بالمللي ثانية.
res.cookie('cart', cartId, {
    httpOnly: true,
    sameSite: 'strict',
    secure: true,
    maxAge: 30 * 60 * 1000, // 30 دقيقة
});
challenge icon

التحدي

سهل

عزّز حماية ملف تعريف الارتباط sid، لكي لا ترسله المتصفحات إلا إلى المكان المناسب، وتحذفه بعد يوم واحد.

  1. في server.js، ضمن POST /login، أضف خيارات الأمان الأربعة هذه إلى ملف تعريف الارتباط sid:
    • httpOnly، لكي لا تتمكن النصوص البرمجية في الصفحة من قراءته. هذا الخيار موجود بالفعل.
    • اضبط sameSite على 'lax'، لكي لا يرسله المتصفح مع الطلبات التي تبدأها مواقع أخرى، إلا إذا نقر المستخدم على رابط إلى موقعك.
    • secure، لكي لا يُرسل إلا عبر HTTPS.
    • اضبط maxAge على يوم واحد. قيمة maxAge بالمللي ثانية، ويعرض الترويسه المدة نفسها بالثواني، لذا يظهر اليوم الواحد كـ Max-Age=86400.
  2. يجب أن يظل تسجيل الدخول يعمل: بعد POST /login، يعيد GET /me المستخدم.

جرّب ذلك: اضغط «العب». أرسل POST /signup، ثم POST /login، مع {"username": "ada", "password": "correct horse"} في كليهما. افتح علامة التبويب «الترويسات» في استجابة تسجيل الدخول. ستظهر قيمة Set-Cookie بالشكل التالي: sid=oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN; Max-Age=86400; Path=/; Expires=Sat, 31 Oct 2026 12:00:00 GMT; HttpOnly; Secure; SameSite=Lax. أرسل GET /me: {"username": "ada"}.

جرّب بنفسك

import express from 'express';
import { randomBytes } from 'node:crypto';
import { parseCookies } from './cookies.js';
import { notes, sessions, 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' });
    }
    const sid = randomBytes(24).toString('base64url');
    await sessions.insertOne({ _id: sid, userId: user._id, createdAt: new Date() });
    res.cookie('sid', sid, { httpOnly: true });
    res.json({ username: user.username });
});

// يعثر على المستخدم المرتبط بملف تعريف ارتباط الجلسة، أو يردّ بالرمز 401.
async function requireUser(req, res, next) {
    const { sid } = parseCookies(req.get('cookie'));
    const session = sid && (await sessions.findOne({ _id: sid }));
    const user = session && (await users.findOne({ _id: session.userId }));
    if (!user) {
        return res.status(401).json({ error: 'Log in first' });
    }
    req.user = { id: user._id.toHexString(), username: user.username };
    next();
}

app.get('/me', requireUser, (req, res) => {
    res.json({ username: req.user.username });
});

app.post('/logout', async (req, res) => {
    const { sid } = parseCookies(req.get('cookie'));
    if (sid) {
        await sessions.deleteOne({ _id: sid });
    }
    res.clearCookie('sid');
    res.status(204).end();
});

// يحصي عدد مرات طلب هذا المتصفح 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', requireUser, async (req, res) => {
    res.json(await notes.find({ owner: req.user.id }).toArray());
});

app.post('/notes', requireUser, async (req, res) => {
    const note = { owner: req.user.id, 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');
});
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

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

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