[Series Thực Chiến E-commerce] Bài 10: Trạm gác "Trùm Cuối" - Phân quyền Admin & Bịt lỗ hổng Data
Chào anh em!
Ở Bài 9 hôm trước, mình đã cố tình để lại một cái "hố bom" to đùng khi viết API lấy danh sách toàn bộ người dùng (getUsers). Lỗi thứ nhất là bốc hết cả password ra ngoài, lỗi thứ hai là ai (kể cả chưa đăng nhập) cũng có thể gọi được API đó.
Hôm nay, chúng ta sẽ làm công việc của một người thợ xây thực thụ: Trát lại cái lỗ hổng rò rỉ dữ liệu, và dựng lên một "trạm gác" phân quyền cực kỳ nghiêm ngặt. Chỉ có tài khoản mang thẻ Admin mới được phép bước qua.
Bắt tay vào fix bug thôi!
1. Bịt lỗ hổng Data: Kỹ thuật .select()
Lỗi đầu tiên, trả về dữ liệu nhạy cảm. Để xử lý việc này trong Mongoose cực kỳ đơn giản. Anh em không cần phải dùng vòng lặp map ra mảng mới làm gì cho cực, chỉ cần dùng hàm .select().
Anh em mở lại file controllers/user.js và sửa lại hàm getUsers như sau:
const asyncHandler = require('express-async-handler');
const User = require('../models/user'); // Nhớ trỏ đúng đường dẫn model nhé
// Controller: Lấy tất cả user
const getUsers = asyncHandler(async (req, res) => {
// 💡 KINH NGHIỆM XƯƠNG MÁU: Dùng dấu trừ (-) đằng trước tên trường để loại bỏ nó
// Ở đây mình ẩn luôn cả password và role (người ngoài không cần biết ai là admin)
const response = await User.find().select('-password -role');
return res.status(200).json({
success: response ? true : false,
users: response,
});
});
module.exports = {
getUsers,
};
Chỉ một chuỗi -password -role ngắn gọn, hệ thống của anh em đã an toàn hơn 90% trước các pha soi mói data từ Network tab của trình duyệt rồi.
2. Dựng "Trạm Gác" Admin (Middleware)
Bây giờ đến phần quan trọng nhất: Phân quyền (Role-based access control).
Anh em nhớ lại ở Bài 5, chúng ta đã có một cái chốt chặn tên là verifyAccessToken. Chốt này có nhiệm vụ giải mã token và gắn thông tin user vào req.user. Lúc tạo token, mình đã khéo léo nhét cả trường role vào đó rồi ({ _id: uid, role }).
Giờ mình tận dụng cái role đó để viết chốt chặn số 2. Anh em mở file middlewares/verifyToken.js và thêm đoạn này vào dưới hàm verify cũ:
const asyncHandler = require('express-async-handler');
// (Hàm verifyAccessToken nằm ở đây...)
// Middleware kiểm tra quyền admin
const isAdmin = asyncHandler((req, res, next) => {
// Bốc cái role từ req.user (do middleware verifyAccessToken truyền sang)
const { role } = req.user;
// Nếu không phải admin thì tiễn khách luôn, không lằng nhằng
if (role !== 'admin') {
return res.status(401).json({
success: false,
mes: 'REQUIRE ADMIN ROLE - Bạn không có quyền truy cập!',
});
}
// Nếu đúng là admin, mở cổng cho đi tiếp vào Controller (hàm getUsers)
next();
});
module.exports = {
verifyAccessToken, // Đừng quên export hàm cũ nhé
isAdmin,
};
3. Lắp ráp "Trạm gác kép" vào Router
Cửa đã đúc xong, giờ đem ra lắp vào lối đi. Anh em mở file routers/user.js:
const express = require('express');
const router = express.Router();
const ctrls = require('../controllers/user');
// Import cả 2 cái trạm gác vào
const { verifyAccessToken, isAdmin } = require('../middlewares/verifyToken');
// 💡 LƯU Ý CỰC KỲ QUAN TRỌNG: Thứ tự đặt Middleware!
// Phải chạy verifyAccessToken TRƯỚC để biết thằng này là ai (lấy được req.user)
// Rồi mới chạy isAdmin SAU để xem thằng này có quyền không.
// Đặt ngược lại là app crash tung tóe ráng chịu nha!
router.get('/getalluser', [verifyAccessToken, isAdmin], ctrls.getUsers);
module.exports = router;
(Tip nhỏ: Anh em dùng mảng [ ] để gom nhóm các middleware lại nhìn cho code nó sạch sẽ và chuyên nghiệp nhé, không bọc mảng nó vẫn chạy nhưng nhìn hơi rối).
Lời kết
Bật Postman lên test thử ngay và luôn:
- Gửi request mà không có token -> Bị chửi Require authentication! (do
verifyAccessTokenchặn). - Lấy token của một user bình thường nhét vào Header rồi gửi -> Bị chửi REQUIRE ADMIN ROLE (do
isAdminchặn). - Đổi
rolecủa một user trong MongoDB thànhadmin, sinh token mới rồi gửi -> Pằng! Data trả về mượt mà, không thấy password đâu.
Thế mới gọi là làm Backend thực chiến anh em ạ!
Xong phần lấy danh sách rồi, Admin thấy có user rác hoặc user vi phạm chính sách thì phải có nút Xóa (Delete) đúng không? Bài sau, chúng ta sẽ bắt tay vào dọn dẹp data dư thừa.
All rights reserved