تسجيل الخروج
جزء من قسم المصادقة والأمان في رحلة Node.js على Coddy. الدرس 12 من 24.
إن مسح ملف تعريف الارتباط يطلب من المتصفح فقط نسيان المعرّف. أما كل من نسخ المعرّف، سواء من جهاز كمبيوتر مشترك أو من ملف تعريف ارتباط مسروق، فما زال يملك مفتاحًا صالحًا. يجب أن يؤدي تسجيل الخروج إلى حذف الجلسة على الخادم.
تطلب res.clearCookie(name) من المتصفح إسقاط ملف تعريف ارتباط. وتعني الحالة 204 «لا يوجد محتوى» أن العملية نجحت وأنه لا يوجد شيء لإرساله: res.status(204).end().
إليك مسارًا ذا صلة يسجّل خروج المستخدم من جميع الأجهزة دفعة واحدة:
app.post('/logout-everywhere', requireUser, async (req, res) => {
await sessions.deleteMany({ userId: new ObjectId(req.user.id) });
res.clearCookie('sid');
res.status(204).end();
});التحدي
سهلأضف طريقة لتسجيل الخروج. حذف ملف تعريف الارتباط وحده لا يكفي، لأن أي شخص نسخ معرّف الجلسة قد يواصل استخدامه. لذلك يحذف الخادم الجلسة أيضًا.
- في
server.js، أضفPOST /logout. لا يحتاج إلى متن للطلب. - اقرأ ملف تعريف الارتباط
sid. إذا وُجد، فاحذف الجلسة التي لها_idهذا منsessions. - أخبر المتصفح أن ينسى ملف تعريف الارتباط
sid. - أرسل استجابة بالحالة 204 ومن دون متن. تعني الحالة 204 No Content أن العملية نجحت وليس هناك شيء لإرساله. أرسل استجابة بالحالة 204 حتى عندما لا يتضمن الطلب ملف تعريف الارتباط
sid.
جرّب ذلك: اضغط «العب». أرسل POST /signup، ثم POST /login، مع {"username": "ada", "password": "correct horse"} في كليهما. يعيد GET /me القيمة {"username": "ada"}. أرسل POST /logout: الحالة 204 ومتن فارغ. أرسل GET /me مرة أخرى: الحالة 401 و{"error": "Log in first"}. افتح البيانات: مجموعة sessions فارغة. أرسل POST /logout مرة أخرى: تظل الحالة 204.
جرّب بنفسك
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 });
});
// يحصي عدد المرات التي أرسل فيها هذا المتصفح طلب 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