Menu
CoddyTech

Chỉ ghi chú của bạn

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 18 trên 24.

Đăng nhập xác minh bạn là ai, chứ không chứng minh ghi chú thuộc về bạn. GET /notes/:id chỉ tìm ghi chú theo id, nên bất kỳ ai đã đăng nhập và có id, từ liên kết được chia sẻ hoặc nhật ký, đều có thể đọc ghi chú đó. Việc kiểm tra chủ sở hữu gọi là phân quyền.

Đưa chủ sở hữu vào chính truy vấn để cơ sở dữ liệu không bao giờ trả về tài liệu của người khác. Trả về 404, không phải 403, khi không tìm thấy: 403 sẽ cho khách truy cập biết ghi chú đó tồn tại.

const order = await orders.findOne({
    _id: new ObjectId(req.params.id),
    customer: req.user.id,
});
if (!order) {
    return res.status(404).json({ error: 'Order not found' });
}

Mọi truy vấn chạm đến dữ liệu của một người dùng đều cần bộ lọc: đọc, cập nhật và xóa.

challenge icon

Thử thách

Dễ

Chương này quay lại cách đăng nhập bằng phiên từ chương 2, và máy chủ đã phát triển thêm. Giờ đây, máy chủ có một trang tại GET /, các tuyến xử lý một ghi chú tại GET /notes/:id và DELETE /notes/:id, cùng một tài khoản quản trị viên. Hai tuyến /notes/:id có một lỗ hổng: chúng chỉ tìm ghi chú bằng id. Bất kỳ người dùng nào đã đăng nhập và biết id đều có thể đọc hoặc xóa ghi chú của người khác. Hãy khắc phục lỗ hổng này.

  1. Trong server.js, ở GET /notes/:id, hãy tìm ghi chú dựa trên cả _id và owner. Chủ sở hữu phải là người dùng đã đăng nhập, req.user.id.
  2. Làm tương tự trong DELETE /notes/:id, để không ai có thể xóa ghi chú không thuộc sở hữu của mình.
  3. Khi không có ghi chú nào khớp, cả hai tuyến đã phản hồi với trạng thái 404 và {"error": "Note not found"}. Cũng giữ nguyên phản hồi đó cho ghi chú của người khác. Trạng thái 403 sẽ cho khách biết ghi chú đó tồn tại.

Thử nhé: Nhấn Run.

  • Gửi POST /signup, sau đó POST /login, cả hai đều dùng {"username": "ada", "password": "correct horse"}.
  • Gửi POST /notes với {"text": "Safe code 4417"}, rồi sao chép _id từ phản hồi. GET /notes/ theo sau là id đó sẽ trả về ghi chú.
  • Đăng ký rồi đăng nhập bằng {"username": "bob", "password": "correct horse"}. Gửi GET /notes/ theo sau là id của ada: trạng thái 404 và {"error": "Note not found"}. Gửi DELETE /notes/ theo sau là cùng id đó: cũng trả về 404.
  • Đăng nhập lại với tư cách ada, dùng phần nội dung từ bước đầu tiên. Ghi chú của cô ấy vẫn còn đó. Giờ hãy DELETE ghi chú đó: trạng thái 204.

Tự làm thử

import express from 'express';
import { ObjectId } from 'mongodb';
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());

// Tài khoản quản trị duy nhất. Mật khẩu của tài khoản này lấy từ môi trường.
await users.insertOne({ username: 'admin', role: 'admin', passwordHash: await hashPassword(process.env.ADMIN_PASSWORD) });

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,
        sameSite: 'lax',
        secure: true,
        maxAge: 24 * 60 * 60 * 1000,
    });
    res.json({ username: user.username });
});

// Tìm người dùng qua cookie phiên, hoặc 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, role: user.role };
    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();
});

// Hiển thị ghi chú của bạn trên một trang.
app.get('/', requireUser, async (req, res) => {
    const list = await notes.find({ owner: req.user.id }).toArray();
    const items = list.map((note) => `<li>${note.text}</li>`).join('');
    res.send(`<!doctype html><title>Vault</title><h1>${req.user.username}'s notes</h1><ul>${items}</ul>`);
});

// 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.get('/notes/:id', requireUser, async (req, res) => {
    if (!ObjectId.isValid(req.params.id)) {
        return res.status(404).json({ error: 'Note not found' });
    }
    const note = await notes.findOne({ _id: new ObjectId(req.params.id) });
    if (!note) {
        return res.status(404).json({ error: 'Note not found' });
    }
    res.json(note);
});

app.delete('/notes/:id', requireUser, async (req, res) => {
    if (!ObjectId.isValid(req.params.id)) {
        return res.status(404).json({ error: 'Note not found' });
    }
    const { deletedCount } = await notes.deleteOne({ _id: new ObjectId(req.params.id) });
    if (!deletedCount) {
        return res.status(404).json({ error: 'Note not found' });
    }
    res.status(204).end();
});

// Dành cho quản trị viên: số lượng tài khoản hiện có.
app.get('/admin', requireUser, async (req, res) => {
    res.json({ users: await users.countDocuments() });
});

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