דגלי Cookie
חלק מהיחידה אימות ואבטחה במסלול ה-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 דקות
});אתגר
קלהקשח את קובץ ה-cookie של sid, כדי שהדפדפנים ישלחו אותו רק למקומות המתאימים וימחקו אותו לאחר יום אחד.
- ב-
server.js, בתוךPOST /login, הגדר לקובץ ה-cookie שלsidאת ארבע האפשרויות האלה:httpOnly, כדי שסקריפטים בדף לא יוכלו לקרוא אותו. האפשרות הזאת כבר מוגדרת.- הגדר את
sameSiteל-'lax', כדי שהדפדפן לא יכלול אותו בבקשות שאתרים אחרים יוזמים, אלא אם המשתמש עוקב אחר קישור לאתר שלך. secure, כדי שהוא יועבר רק דרך HTTPS.maxAgeשל יום אחד. הערך שלmaxAgeהוא במילישניות, והכותרת מציגה את אותו משך זמן בשניות, לכן יום אחד מופיע בתורMax-Age=86400.
- ההתחברות עדיין צריכה לעבוד: אחרי
POST /login, הבקשהGET /meמחזירה את המשתמש.
נסו זאת: לחצו על Run. שלחו POST /signup, ואז POST /login, ובשניהם השתמשו ב-{"username": "ada", "password": "correct horse"}. פתחו את לשונית Headers בתשובת ההתחברות. הערך של 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');
});
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה אימות ואבטחה
תרגלו בעצמכם: Express אונליין