علامات ملفات تعريف الارتباط
جزء من قسم المصادقة والأمان في رحلة 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 دقيقة
});التحدي
سهلعزّز حماية ملف تعريف الارتباط sid، لكي لا ترسله المتصفحات إلا إلى المكان المناسب، وتحذفه بعد يوم واحد.
- في
server.js، ضمنPOST /login، أضف خيارات الأمان الأربعة هذه إلى ملف تعريف الارتباطsid:httpOnly، لكي لا تتمكن النصوص البرمجية في الصفحة من قراءته. هذا الخيار موجود بالفعل.- اضبط
sameSiteعلى'lax'، لكي لا يرسله المتصفح مع الطلبات التي تبدأها مواقع أخرى، إلا إذا نقر المستخدم على رابط إلى موقعك. secure، لكي لا يُرسل إلا عبر HTTPS.- اضبط
maxAgeعلى يوم واحد. قيمةmaxAgeبالمللي ثانية، ويعرض الترويسه المدة نفسها بالثواني، لذا يظهر اليوم الواحد كـMax-Age=86400.
- يجب أن يظل تسجيل الدخول يعمل: بعد
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');
});
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس المصادقة والأمان
2الجلسات وملفات تعريف الارتباط
التذكّر باستخدام ملفات تعريف الارتباطالبحث عن جلسةجلسات تسجيل الدخولالمسارات المحمية بتسجيل الدخولتدرّب بنفسك: Playground لـ Express