נתיבים מאחורי התחברות
חלק מהיחידה אימות ואבטחה במסלול ה-Node.js של Coddy. שיעור 11 מתוך 24.
כל נתיב פרטי זקוק לחיפוש הפגישה מתוך GET /me. אם מעתיקים אותו לכל נתיב, פעם אחת ישכחו להוסיף אותו, ואז הנתיב הזה יהיה פתוח לכולם. במקום זאת, כתבו אותו פעם אחת בתור תוכנת ביניים.
תוכנת הביניים פועלת לפני הנתיב. כשהיא מציבה משהו בתוך req, כמו req.user, הנתיב קורא אותו משם. ה־_id של משתמש הוא ObjectId; user._id.toHexString() מחזירה אותו כטקסט. היא משיבה ועוצרת, או מציבה בתוך req את מה שמצאה וקוראת ל־next(). העבירו אותה לכל נתיב שזקוק לה, לפני המטפל:
function requireAdult(req, res, next) {
if (Number(req.query.age) < 18) {
return res.status(403).json({ error: 'Adults only' });
}
req.adult = true;
next();
}
app.get('/wines', requireAdult, (req, res) => res.json(wines));חשוב להשתמש ב־return: בלעדיו, הנתיב היה מופעל גם כן.
אתגר
קלההערות עדיין פתוחות לכולם. הפכו אותן לפרטיות: רק משתמש מחובר יכול לקרוא הערות או להוסיף אותן, וכל משתמש רואה רק את ההערות שלו.
- ב־
server.js, כתבו פונקציית תוכנת בינייםrequireUser(req, res, next). העבירו אליה את חיפוש ההפעלה מ־GET /me: קראו את עוגייתsid, מצאו את ההפעלה ואז מצאו את המשתמש שלה.- כשאין משתמש כזה, השיבו עם סטטוס 401 ועם
{"error": "Log in first"}, ועצרו שם. - אחרת, הציבו את המשתמש בבקשה בתור
req.user, אובייקט עם שני שדות, ואז קראו ל־next(). שני השדות הםid, ה־_idשל המשתמש כטקסט, ו־username. המתודהtoHexString()של ObjectId מחזירה את הערך שלו כטקסט, למשל"66f1c2a9e4b0a1b2c3d4e5f6".
- כשאין משתמש כזה, השיבו עם סטטוס 401 ועם
- העבירו את
requireUserאלGET /me, אלGET /notesואלPOST /notes, לפני הפונקציה של כל נתיב. לאחר מכן,GET /meמשיב עם{"username": "ada"}, שנקרא מתוךreq.user. - ב־
POST /notes, שמרו כל הערה עם שדהownerשמכיל אתreq.user.id. התשובה היא ההערה החדשה:{"owner": "66f1c2a9e4b0a1b2c3d4e5f6", "text": "Door code 1234", "_id": "66f1c2b0e4b0a1b2c3d4e5f7"} - ב־
GET /notes, החזירו רק את ההערות שה־ownerשלהן הוא המשתמש המחובר.
נסו זאת: לחצו על Run. שלחו GET /notes: הסטטוס הוא 401 ו־{"error": "Log in first"}. שלחו POST /signup, ואז POST /login, ובשניהם השתמשו ב־{"username": "ada", "password": "correct horse"}. שלחו POST /notes עם {"text": "Door code 1234"}: הסטטוס הוא 201 והתגובה כוללת את ההערה עם owner שלה. שלחו GET /notes: רשימה שמכילה את ההערה היחידה הזאת. עכשיו הירשמו והתחברו עם {"username": "bob", "password": "correct horse"}. הבקשה GET /notes מחזירה [], כי bob עדיין לא כתב דבר.
נסו בעצמכם
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 });
});
app.get('/me', async (req, res) => {
const { sid } = parseCookies(req.get('cookie'));
const session = sid && (await sessions.findOne({ _id: sid }));
if (!session) {
return res.status(401).json({ error: 'Log in first' });
}
const user = await users.findOne({ _id: session.userId });
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');
});
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה אימות ואבטחה
2Sessions ו-Cookies
זכירה באמצעות Cookiesחיפוש SessionSessions להתחברותנתיבים מאחורי התחברותתרגלו בעצמכם: Express אונליין