Cờ cookie
Thuộc phần Xác thực và bảo mật trong hành trình Node.js của Coddy. Bài học 13 trên 24.
Cookie không có cờ sẽ được gửi qua HTTP thông thường, đi kèm các yêu cầu do trang web khác khởi tạo và tồn tại cho đến khi trình duyệt đóng. Bốn tùy chọn trên res.cookie giúp cookie an toàn hơn:
httpOnly: tập lệnh trên trang không thể đọc cookie này.sameSite:'lax'không gửi cookie kèm các yêu cầu do trang web khác gửi, trừ khi người dùng nhấp vào liên kết;'strict'cũng không gửi cookie trong trường hợp đó.secure: chỉ gửi qua HTTPS, không bao giờ gửi qua HTTP thông thường, nơi người khác trên mạng có thể đọc cookie.maxAge: thời gian cookie tồn tại, tính bằng mili giây.
res.cookie('cart', cartId, {
httpOnly: true,
sameSite: 'strict',
secure: true,
maxAge: 30 * 60 * 1000, // 30 phút
});Thử thách
DễSiết chặt cookie sid để trình duyệt chỉ gửi cookie này đúng nơi cần thiết và quên cookie sau một ngày.
- Trong
server.js, ởPOST /login, đặt bốn tùy chọn sau cho cookiesid:httpOnly, để các tập lệnh trên trang không thể đọc cookie. Tùy chọn này đã có sẵn.- Đặt
sameSitethành'lax', để trình duyệt không gửi cookie trong các yêu cầu do trang web khác khởi tạo, trừ khi người dùng nhấp vào liên kết dẫn đến trang web của bạn. secure, để cookie chỉ được gửi qua HTTPS.maxAgelà một ngày.maxAgetính bằng mili giây, còn tiêu đề hiển thị cùng khoảng thời gian đó bằng giây, nên một ngày được thể hiện làMax-Age=86400.
- Đăng nhập vẫn phải hoạt động: sau
POST /login,GET /metrả về người dùng.
Thử ngay: Nhấn Chạy. Gửi POST /signup, rồi POST /login, cả hai đều có {"username": "ada", "password": "correct horse"}. Mở thẻ Headers trong phản hồi đăng nhập. Set-Cookie có dạng sid=oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN; Max-Age=86400; Path=/; Expires=Sat, 31 Oct 2026 12:00:00 GMT; HttpOnly; Secure; SameSite=Lax. Gửi GET /me: {"username": "ada"}.
Tự làm thử
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 });
});
// Tìm người dùng qua cookie phiên, nếu không có thì trả về 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();
});
// Đếm số lần trình duyệt (browser) này gọi 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 });
});
// Ghi chú: mỗi người dùng chỉ đọc và viết ghi chú của mình.
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');
});
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Xác thực và bảo mật
Tự luyện tập: Playground Express