Menu
CoddyTech

Định tuyến sau khi đăng nhập

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

Mỗi route riêng tư đều cần tra cứu phiên từ GET /me. Nếu sao chép đoạn này vào từng route, sớm muộn bạn cũng sẽ quên một lần, khiến route đó mở cho tất cả mọi người. Thay vào đó, hãy viết đoạn này một lần dưới dạng middleware.

Middleware chạy trước route. Khi middleware gắn thứ gì đó vào req, chẳng hạn như req.user, route sẽ đọc thông tin đó từ đây. _id của người dùng là một ObjectId; user._id.toHexString() chuyển nó thành văn bản. Middleware sẽ trả lời rồi dừng lại, hoặc gắn thông tin tìm được vào req rồi gọi next(). Truyền middleware này vào từng route cần dùng, trước trình xử lý:

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 rất quan trọng: nếu thiếu nó, route cũng sẽ chạy.

challenge icon

Thử thách

Dễ

Ghi chú vẫn mở cho mọi người. Hãy đặt ghi chú ở chế độ riêng tư: chỉ người dùng đã đăng nhập mới có thể đọc hoặc thêm ghi chú, và mỗi người dùng chỉ thấy ghi chú của riêng mình.

  1. Trong server.js, viết hàm middleware requireUser(req, res, next). Chuyển phần tìm phiên đăng nhập từ GET /me vào hàm này: đọc cookie sid, tìm phiên đăng nhập, rồi tìm người dùng tương ứng.
    • Nếu không tìm thấy người dùng, hãy phản hồi với mã trạng thái 401 và {"error": "Log in first"}, rồi dừng lại.
    • Nếu tìm thấy, hãy gán người dùng vào yêu cầu dưới dạng req.user, một đối tượng có hai trường, rồi gọi next(). Hai trường đó là id, giá trị _id của người dùng ở dạng văn bản, và username. Phương thức toHexString() của ObjectId trả về giá trị ở dạng văn bản, chẳng hạn "66f1c2a9e4b0a1b2c3d4e5f6".
  2. Truyền requireUser vào GET /me, GET /notes và POST /notes, trước hàm riêng của từng route. Khi đó, GET /me sẽ phản hồi bằng {"username": "ada"}, lấy từ req.user.
  3. Trong POST /notes, lưu mỗi ghi chú với trường owner chứa req.user.id. Phản hồi là ghi chú mới: {"owner": "66f1c2a9e4b0a1b2c3d4e5f6", "text": "Door code 1234", "_id": "66f1c2b0e4b0a1b2c3d4e5f7"}
  4. Trong GET /notes, chỉ trả về những ghi chú có owner là người dùng đã đăng nhập.

Thử nhé: Nhấn Chạy. Gửi GET /notes: mã trạng thái 401 và {"error": "Log in first"}. Gửi POST /signup, rồi POST /login, cả hai đều dùng {"username": "ada", "password": "correct horse"}. Gửi POST /notes với {"text": "Door code 1234"}: mã trạng thái 201 và ghi chú có owner. Gửi GET /notes: một danh sách chứa ghi chú đó. Bây giờ, hãy đăng ký và đăng nhập bằng {"username": "bob", "password": "correct horse"}. GET /notes trả về [] vì bob chưa viết ghi chú nào.

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

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

// Đếm số lần trình duyệt 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ú. Hiện tại, ai cũng có thể đọc và viết ghi chú.
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');
});
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