جلسات تسجيل الدخول
جزء من قسم المصادقة والأمان في رحلة 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 });التحدي
سهلتفتح هذه الخطوة مشروع واجهة API للملاحظات الخاصة المُعدّ مسبقًا للدورة، بدلًا من ملفات مشروعك من درس التذكّر باستخدام ملفات تعريف الارتباط (بما فيها server.js). يبقى عملك السابق في درسه الخاص. اقرأ الملفات المزوّدة قبل تعديل server.js. تحتوي هذه النسخة على مسارات التسجيل وتسجيل الدخول للملاحظات الخاصة.
يعمل تسجيل الدخول، لكن الطلب التالي يكون قد نسي بالفعل من سجّل الدخول. اجعل الخادم يتذكّر المستخدم باستخدام جلسة: معرّف عشوائي يُحفَظ على الخادم ويُرسَل إلى المتصفح على شكل ملف تعريف ارتباط.
- في
server.js، استوردrandomBytesمنnode:crypto. استوردsessionsمن./db.jsإلى جانبnotesوusers. يحتويdb.jsالمقفل بالفعل على مجموعةsessions. - في
POST /login، بعد التحقق من كلمة المرور، أنشئ معرّف جلسة: 24 بايتًا عشوائيًا مكتوبًا كنص بترميز base64url. أي 32 حرفًا من الأحرف والأرقام و-و_، مثلoqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN. أنشئ معرّفًا جديدًا عند كل تسجيل دخول. - احفظ الجلسة في
sessionsكمستند ذي حقلين:_id: معرّف الجلسة.userId: قيمة_idالخاصة بالمستخدم الذي سجّل الدخول.
- أرسل المعرّف إلى المتصفح في ملف تعريف ارتباط اسمه
sid، مع الخيارhttpOnly، لكيلا تتمكن النصوص البرمجية في الصفحة من قراءته. يظل رد تسجيل الدخول{"username": "ada"}. - أضف
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');
});
جميع دروس المصادقة والأمان
2الجلسات وملفات تعريف الارتباط
التذكّر باستخدام ملفات تعريف الارتباطالبحث عن جلسةجلسات تسجيل الدخولالمسارات المحمية بتسجيل الدخولتدرّب بنفسك: Playground لـ Express