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 đó.
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ò đó.
- 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. - Viết hàm
requireRole(role)trả về một middleware. Middleware đó kiểm trareq.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ọinext().
- Nếu giá trị này không phải
- Thêm
requireRole('admin')vàoGET /admin, saurequireUser. Tuyến này đọcreq.user, thuộc tính chỉ được đặt bởirequireUser.
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
1Mật khẩu
Byte và văn bản hexTạo khóa từ mật khẩuSalt mớiKhông bao giờ lưu mật khẩuSo sánh các byte của khóaKiểm tra mật khẩuQuy tắc đăng ký4Quyền sở hữu và an toàn
Chỉ ghi chú của bạnVai trò quản trị viên2Phiên làm việc và cookie
Ghi nhớ bằng cookieTra cứu phiênPhiên đăng nhậpĐịnh tuyến sau khi đăng nhậpTự luyện tập: Playground Express