0

[Series Thực Chiến E-commerce] Bài 4: Chìa khóa vào nhà - Validate Login & Hoàn thiện luồng Đăng nhập

Chào anh em thiện lành!

Ở Bài 3, chúng ta đã biến database thành một cái "két sắt" khá vững chãi: Mật khẩu được băm nát bét (hash), dữ liệu đăng ký thì được rà soát kỹ càng (validate). Vậy khách đăng ký xong thì phải cho người ta đăng nhập (Login) chứ nhỉ?

Hôm nay, chúng ta sẽ viết nốt luồng Login. Bài này tưởng dễ mà lại có vài "trick" bảo mật và tips viết code cực kỳ hay ho mà hồi mới đi làm mình không hề biết. Anh em pha ly cafe rồi chiến tiếp nhé!

1. Dạy Model cách tự nhận diện chủ nhân

Bình thường, anh em sẽ query lấy mật khẩu đã mã hóa từ database lên, rồi ném vào Controller để so sánh bằng bcrypt.compare, đúng không?

Không sai, nhưng để code "sạch" và xịn hơn, người ta thường áp dụng pattern "Fat Model, Skinny Controller" (Model dày, Controller mỏng). Tức là những logic liên quan trực tiếp đến dữ liệu của User thì hãy để chính User tự làm.

Anh em mở file models/user.js, thêm đoạn code này vào để tạo ra một Instance Method (phương thức tùy chỉnh):

const bcrypt = require('bcrypt'); // Đã import ở bài trước rồi nhé

// Thêm method này để so sánh password
userSchema.methods = {
    isCorrectPassword: async function (password) {
        // 'password' là chuỗi user nhập vào lúc login
        // 'this.password' là chuỗi đã mã hóa lưu trong DB
        return await bcrypt.compare(password, this.password);
    }
};

Nhờ cái này, tí nữa vào Controller mình chỉ cần gọi user.isCorrectPassword('123456') là xong, code đọc như một câu tiếng Anh vậy!

2. Gác cổng lúc Đăng nhập (Validation)

Cũng giống như lúc đăng ký, chúng ta không được phép tin tưởng data gửi lên lúc đăng nhập. Dù form chỉ có Email và Password, vẫn phải check!

Mở file validation/validateRegister.js ra (ở bài sau nếu rảnh anh em nên đổi tên file này thành authValidator.js cho chuẩn nghĩa hơn nhé), và thêm mảng validation này vào:

// ... (phần code validateRegister của bài trước)

const validateLogin = [
    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ự'),

    // Hứng lỗi và trả về
    (req, res, next) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({
                success: false,
                errors: errors.array().map(err => err.msg)
            });
        }
        next();
    }
];

// Nhớ export cả validateLogin ra nhé
module.exports = { validateRegister, validateLogin };

3. Não bộ Controller: Logic Đăng nhập

Giờ là lúc lắp ráp mọi thứ. Mở controllers/user.js lên và thêm hàm login này:

const login = asyncHandler(async (req, res) => {
    const { email, password } = req.body;

    // 1. Check xem có gửi đủ data không (Dù middleware đã check, nhưng check thừa còn hơn thiếu)
    if (!email || !password) {
        return res.status(400).json({
            success: false,
            message: 'Missing inputs',
        });
    }

    // 2. Tìm user trong DB theo email
    const response = await User.findOne({ email });

    // 💡 CHIÊU BẢO MẬT #1: Không bao giờ nói cho user biết họ sai email hay sai mật khẩu!
    if (!response) {
        return res.status(401).json({
            success: false,
            message: 'Invalid credentials!', // Thông tin đăng nhập không hợp lệ
        });
    }

    // 3. Nếu tìm thấy email, check xem password có khớp không
    const isMatch = await response.isCorrectPassword(password);

    if (!isMatch) {
        return res.status(401).json({
            success: false,
            message: 'Invalid credentials!', // Vẫn trả về câu này y hệt phía trên
        });
    }

    // 💡 CHIÊU TRÍCH XUẤT DATA #2: Lược bỏ các trường nhạy cảm trước khi trả về client
    // Dùng destructuring (phân rã) để tách password và role ra, phần còn lại gom vào userData
    const { password: _, role, ...userData } = response.toObject();

    // 4. Trả kết quả thành công
    return res.status(200).json({
        success: true,
        userData, // Chỉ trả về những gì an toàn
    });
});

module.exports = { register, login }; // Đừng quên export

Mình giải thích thêm 2 chiêu cực hay ở trên nhé:

Chiêu số 1: Nếu anh em báo "Email không tồn tại" hoặc "Sai mật khẩu", hacker sẽ dùng nó để dò xem hệ thống của mình có những email nào (User Enumeration Attack). Cứ trả về "Invalid credentials" (Thông tin không hợp lệ) chung chung thôi.

Chiêu số 2: Thằng Mongoose object nó có rất nhiều hàm linh tinh ẩn bên trong, mình dùng .toObject() để biến nó thành một object Javascript thuần. Sau đó dùng cú pháp destructuring loại bỏ trường passwordrole đi. Trả thẳng cục response ra là lộ hết rủi ro đấy!

4. Setup Route Đăng nhập

Cuối cùng, mở routes/user.js ra và nối dây cho hàm login nào:

const express = require('express');
const router = express.Router();
// Import controller và validator
const ctrls = require('../controllers/user');
const { validateRegister, validateLogin } = require('../validation/validateRegister'); 

router.post('/register', validateRegister, ctrls.register);
// Chèn chốt chặn validateLogin trước khi gọi vào Controller
router.post('/login', validateLogin, ctrls.login); 

module.exports = router;

Bật Postman lên, bắn thử API /api/user/login với email và password vừa đăng ký ở Bài 2 xem sao. Nếu thấy trả về data ngon lành, thiếu mất cái passwordrole là anh em đã thành công rồi đấy!

Lời kết

Vậy là tính năng Đăng nhập đã chạy mượt. Nhưng khoan đã... nếu anh em làm việc với Front-end (ReactJS, VueJS...), làm sao server biết thằng user vừa đăng nhập thành công là ai ở những request sau (ví dụ: request xem giỏ hàng)? Không lẽ bắt nó gửi kèm Email/Password liên tục?

Đó là lúc chúng ta cần đến "tấm thẻ căn cước" thần thánh mang tên JWT (JSON Web Token).

Bài viết cũng hòm hòm rồi, anh em nhớ lưu lại thực hành. Có lỗi gì cứ quăng vào comment mình cùng xem nhé! hẹn anh em ở bài số 5 nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí