Menu
CoddyTech

Vai trò quản trị viê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 19 trên 24.

GET /admin chỉ kiểm tra xem có người đăng nhập hay chưa, nên mọi thành viên đều thấy được trang này. Vai trò được lưu trên người dùng, như 'member' hoặc 'admin', cho biết họ có thể làm gì. Một hàm tạo middleware, tức là hàm trả về một middleware, kiểm tra một vai trò cho mỗi tuyến:

function requirePlan(plan) {
    return (req, res, next) => {
        if (req.user.plan !== plan) {
            return res.status(402).json({ error: `Needs the ${plan} plan` });
        }
        next();
    };
}

app.get('/exports', requireUser, requirePlan('pro'), sendExports);

Máy chủ luôn thiết lập vai trò. Khi đăng ký, hệ thống chỉ sao chép những trường mà nó mong đợi từ req.body, nên khách truy cập gửi "role": "admin" cũng không nhận được gì từ việc đó.

challenge icon

Thử thách

Dễ

GET /admin cho biết có bao nhiêu tài khoản, và hiện tại mọi thành viên đã đăng nhập đều xem được. Chỉ quản trị viên mới được xem. Máy chủ đặt vai trò cho mỗi người dùng, rồi tuyến này kiểm tra vai trò đó.

  1. Trong server.js, ở POST /signup, lưu mỗi người dùng mới với "role": "member". Không bao giờ lấy vai trò từ phần thân yêu cầu: khách truy cập gửi "role": "admin" vẫn trở thành thành viên.
  2. Viết hàm requireRole(role) trả về một middleware. Middleware đó kiểm tra req.user.role:
    • Nếu giá trị này không phải role, hãy phản hồi với trạng thái 403 và {"error": "Not allowed"}. 403 Forbidden nghĩa là máy chủ biết bạn là ai, nhưng bạn không được phép làm việc này. 401 nghĩa là máy chủ không biết bạn là ai.
    • Nếu giá trị này là role, hãy gọi next().
  3. Thêm requireRole('admin') vào GET /admin, sau requireUser. Tuyến này đọc req.user, thuộc tính chỉ được đặt bởi requireUser.

Máy chủ đã tạo một tài khoản quản trị viên khi khởi động. Tên người dùng là admin, còn mật khẩu là admin-pass-for-tests, lấy từ biến môi trường ADMIN_PASSWORD.

Thử nhé: Nhấn Chạy. Gửi GET /admin: trạng thái 401 và {"error": "Log in first"}. Bây giờ, một người dùng tên mallory thử tự cấp cho mình quyền quản trị viên. Gửi POST /signup với {"username": "mallory", "password": "correct horse", "role": "admin"}, rồi gửi POST /login với {"username": "mallory", "password": "correct horse"}. Gửi GET /admin: trạng thái 403 và {"error": "Not allowed"}, vì trường bổ sung không cấp cho cô ấy quyền quản trị viên. Gửi POST /login với {"username": "admin", "password": "admin-pass-for-tests"}, rồi gửi GET /admin: {"users": 2}.

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 admin 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), owner: req.user.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), owner: req.user.id });
    if (!deletedCount) {
        return res.status(404).json({ error: 'Note not found' });
    }
    res.status(204).end();
});

// Dành cho admin: 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');
});

Tất cả bài học trong Xác thực và bảo mật

Tự luyện tập: Playground Express