Menu
CoddyTech

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
});
challenge icon

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.

  1. Trong server.js, ở POST /login, đặt bốn tùy chọn sau cho cookie sid:
    • 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 sameSite thà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.
    • maxAge là một ngày. maxAge tí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.
  2. Đăng nhập vẫn phải hoạt động: sau POST /login, GET /me trả 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');
});
quiz iconTự kiểm tra

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