[Series Thực Chiến E-commerce] Bài 3: Đừng để Hacker "hỏi thăm" - Mã hóa Password & "Gác cổng" Data
Chào mừng anh em quay lại với series tự build backend E-commerce!
Ở cuối Bài 2, mình có để lại một "lỗ hổng" to đùng: Lưu password của user dưới dạng nguyên thủy (plain-text). Nhìn vào database mà thấy rành rành mật khẩu iloveyou123 của khách thì thực sự là một "tội ác" trong nghề code. Lỡ một ngày đẹp trời database bị leak, hacker sẽ có trọn vẹn thông tin đăng nhập của toàn bộ user.
Thứ hai, anh em tuyệt đối không bao giờ được tin tưởng dữ liệu từ client gửi lên. Front-end có validate kỹ đến mấy thì hacker vẫn có thể dùng Postman bắn data rác (email sai định dạng, sđt giả...) thẳng vào API của mình.
Vì vậy, trong Bài 3 này, chúng ta sẽ giải quyết triệt để 2 vấn đề trên: Băm nát (Hash) Password và Xây dựng chốt chặn Validation. Lên đồ thôi!
1. Mã hóa Password: Biến "iloveyou123" thành chuỗi vô nghĩa
Để mã hóa mật khẩu, chúng ta sẽ dùng thư viện bcrypt đã cài ở Bài 1. Thay vì viết logic mã hóa ở Controller, mình sẽ dùng một tính năng cực hay của Mongoose là Middleware Hook (pre('save')). Nghĩa là: Cứ mỗi khi có hành động save (lưu) một document User vào database, Mongoose sẽ tự động chặn lại một nhịp để mã hóa password trước, rồi mới lưu.
Anh em mở file models/user.js và thêm đoạn code này vào ngay trước dòng module.exports:
const bcrypt = require('bcrypt'); // Nhớ require bcrypt ở đầu file nhé
// Hash password trước khi lưu
userSchema.pre('save', async function (next) {
// Nếu password không bị thay đổi (ví dụ lúc update user info) thì bỏ qua, đi tiếp
if (!this.isModified('password')) return next();
// Tạo "muối" (salt) độ phức tạp 10 để trộn vào password
const salt = await bcrypt.genSalt(10);
// Băm password kèm muối
this.password = await bcrypt.hash(this.password, salt);
next(); // Xử lý xong thì gọi next() để đi tiếp vòng đời của Mongoose
});
Chỉ vài dòng ngắn gọn, từ nay bất cứ khi nào anh em gọi User.create(), mật khẩu tự động biến thành dạng kiểu $2b$10$X7... siêu an toàn.
2. Dựng "chốt kiểm tra" Validation cho API
Thay vì viết mấy chục dòng if...else check từng field ở trong Controller, mình sẽ dùng thư viện express-validator để code xịn sò và sạch sẽ hơn.
Anh em tạo một thư mục mới là validation, sau đó tạo file validateRegister.js bên trong:
const { check, validationResult } = require('express-validator');
// Middleware validation cho đăng ký
const validateRegister = [
check('firstname')
.notEmpty().withMessage('Firstname không được để trống')
.isLength({ min: 2 }).withMessage('Firstname phải có ít nhất 2 ký tự')
.matches(/^[\\p{L} ]+$/u).withMessage('Firstname chỉ được chứa chữ cái'),
check('lastname')
.notEmpty().withMessage('Lastname không được để trống')
.isLength({ min: 2 }).withMessage('Lastname phải có ít nhất 2 ký tự')
.matches(/^[\\p{L} ]+$/u).withMessage('Lastname chỉ được chứa chữ cái'),
check('email')
.notEmpty().withMessage('Email không được để trống')
.isEmail().withMessage('Email không hợp lệ'),
check('password')
.notEmpty().withMessage('Mật khẩu không được để trống')
.isLength({ min: 6 }).withMessage('Mật khẩu phải có ít nhất 6 ký tự'),
check('mobile')
.notEmpty().withMessage('Số điện thoại không được để trống')
// Regex check số điện thoại nhà mạng VN
.matches(/^(0[3-9])+([0-9]{8})$/).withMessage('Số điện thoại không hợp lệ'),
// Middleware cuối cùng để hứng lỗi nếu có
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array().map(err => err.msg) // Trả về mảng các câu thông báo lỗi cho Front-end
});
}
next(); // Nếu không có lỗi gì thì cho phép đi tiếp vào Controller
}
];
module.exports = validateRegister;
Kinh nghiệm: Viết Validation thành Middleware thế này giúp anh em tái sử dụng dễ dàng và giữ cho Controller (não bộ ứng dụng) chỉ tập trung xử lý logic nghiệp vụ chính, không bị rác bởi code check lặt vặt.
3. Lắp ráp "Chốt chặn" vào Route
Có chốt rồi thì phải đem ra đường đặt. Anh em mở file routers/user.js và nhét cái middleware vừa tạo vào giữa:
const router = require('express').Router();
const ctrls = require('../controllers/user');
const validateRegister = require('../validation/validateRegister'); // Import vào
// Đặt validateRegister đứng gác ngay trước khi vào ctrls.register
router.post('/register', validateRegister, ctrls.register);
module.exports = router;
4. Lưới hứng lỗi cuối cùng (Global Error Handling)
Có một trường hợp thế này: User gõ sai đường dẫn (ví dụ /api/user/regisssster), hoặc trong quá trình code mình làm văng ra một cái Exception nào đó nhưng quên bắt lại. Để app không bị crash chết đứng và trả về lỗi thô kệch, ta cần một bộ xử lý lỗi tổng tổng thể (Global Error Handling).
Anh em sẽ có 2 hàm notFound (bắt route linh tinh) và errHandler (bắt lỗi hệ thống văng ra) ở trong thư mục middlewares. Cách lắp ráp nó vào trạm điều phối trung tâm cực kỳ quan trọng.
Mở file routers/index.js và ráp như sau:
const userRouter = require('./user');
const { notFound, errHandler } = require('../middlewares/errHandler');
const initRouter = (app) => {
app.use('/api/user', userRouter);
// QUAN TRỌNG: Hai cái hứng lỗi này PHẢI nằm ở cuối cùng, sau khi đã khai báo hết các routes hợp lệ
app.use(notFound); // Nếu chạy qua hết các route trên mà không khớp, sẽ rơi vào đây
app.use(errHandler); // Hứng mọi lỗi được throw ra từ toàn bộ hệ thống
}
module.exports = initRouter;
Nhớ quy tắc vàng này nhé: Middleware Error Handling phải luôn là chốt chặn nằm dưới cùng của ứng dụng!
Tổng kết Bài 3
Vậy là hệ thống của chúng ta đã có thêm "áo giáp". Password được băm nát bằng muối, data rác bị chặn đứng từ vòng gửi xe, và mọi lỗi lầm (nếu có) đều được túm lại xử lý gọn gàng.
Đã đăng ký an toàn rồi thì phải đăng nhập được đúng không anh em? Bài này code khá nhiều nên mình tạm nghỉ ngơi chút. Chuẩn bị tinh thần để triển tiếp Lession 4: Validation Login & Login User (Đăng nhập và cấp Token) nhé!
Mọi người chạy thử nếu vướng chỗ nào thì comment bên dưới để mình support. Happy coding!
All rights reserved