התנתקות
חלק מהיחידה אימות ואבטחה במסלול ה-Node.js של Coddy. שיעור 12 מתוך 24.
ניקוי קובץ ה-cookie רק מבקש מהדפדפן לשכוח את המזהה. מי שהעתיק את המזהה, ממחשב משותף או מקובץ cookie גנוב, עדיין מחזיק במפתח שעובד. התנתקות חייבת למחוק את ההפעלה בשרת.
res.clearCookie(name) מורה לדפדפן להסיר קובץ cookie. הסטטוס 204 No Content פירושו שהפעולה הצליחה ושאין מה לשלוח בחזרה: 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. אם יש כזאת, מחק מתוךsessionsאת ההפעלה עם_idהזה. - הורה לדפדפן לשכוח את העוגייה
sid. - השב עם סטטוס 204 וללא גוף. פירושו של 204 No Content הוא שהפעולה הצליחה ואין מה לשלוח בחזרה. השב עם 204 גם אם בבקשה לא הייתה עוגייה בשם
sid.
נסה את זה: לחץ על Run. שלח POST /signup, ואז POST /login, ובשניהם השתמש ב־{"username": "ada", "password": "correct horse"}. GET /me מחזיר {"username": "ada"}. שלח POST /logout: סטטוס 204 וגוף ריק. שלח שוב GET /me: סטטוס 401 ו־{"error": "Log in first"}. פתח את Data: 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');
});
כל השיעורים ביחידה אימות ואבטחה
תרגלו בעצמכם: Express אונליין