[Series Thực Chiến E-commerce] Bài 5: Tấm thẻ thông hành - Access Token & Refresh Token (Phần 1)
Chào anh em!
Tiếp nối câu chuyện dang dở ở Bài 4, sau khi user đăng nhập thành công, làm sao để server biết "thằng chả" đang gọi API thêm hàng vào giỏ chính là người vừa đăng nhập ban nãy? Không thể nào bắt user cứ chuyển trang là lại gửi email/password lên được, đúng không?
Đó là lúc chúng ta cần đến JWT (JSON Web Token). Hiểu nôm na, sau khi user trình chứng minh thư (email/password) hợp lệ, server sẽ cấp cho họ một "tấm thẻ thông hành" (Token). Lần sau đến, chỉ cần đưa thẻ ra là qua cổng.
Nhưng đời không như mơ, nếu tấm thẻ này bị hacker trộm mất thì sao? Để giải quyết bài toán bảo mật, người ta đẻ ra cơ chế dùng 2 thẻ cùng lúc: Access Token (thẻ dùng tạm, mau hết hạn) và Refresh Token (thẻ gia hạn, sống lâu hơn).
Hôm nay chúng ta sẽ setup bộ đôi quyền lực này nhé!
1. Xưởng in thẻ: Tạo JWT (middlewares/jwt.js)
Trước tiên, anh em mở file .env lên và chèn thêm một cái Secret Key. Đây là "con dấu" độc quyền của server nhà mình, tuyệt đối không để lộ:
JWT_SECRET=huyhoang1234
(Thực tế thì anh em nên gõ random một chuỗi loằng ngoằng dài ngoằng vào cho an toàn nhé).
Tiếp theo, tạo filemiddlewares/jwt.js để viết 2 hàm tạo thẻ:
const jwt = require('jsonwebtoken');
// Thẻ dùng để đi lại hàng ngày (thời hạn ngắn: 3 ngày)
const generateAccessToken = (uid, role) => {
return jwt.sign({ _id: uid, role }, process.env.JWT_SECRET, { expiresIn: '3d' });
};
// Thẻ dùng để đi xin cấp lại thẻ ngày (thời hạn dài: 7 ngày)
const generateRefreshToken = (uid) => {
return jwt.sign({ _id: uid }, process.env.JWT_SECRET, { expiresIn: '7d' });
};
module.exports = { generateAccessToken, generateRefreshToken };
2. Nâng cấp hàm Login: Cấp phát thẻ cho User
Quay lại file controllers/user.js ở bài 4. Khi password đã khớp, thay vì chỉ trả về thông tin user, mình sẽ in thẻ và nhét thẻ vào tay họ.
const asyncHandler = require('express-async-handler');
const User = require('../models/user');
const { generateAccessToken, generateRefreshToken } = require('../middlewares/jwt');
const login = asyncHandler(async (req, res) => {
const { email, password } = req.body;
if (!email || !password) return res.status(400).json({ success: false, message: 'Missing inputs' });
const response = await User.findOne({ email });
if (!response || !(await response.isCorrectPassword(password))) {
return res.status(401).json({ success: false, message: 'Invalid credentials!' });
}
// 1. Tạo cặp Token
const accessToken = generateAccessToken(response._id, response.role);
const refreshToken = generateRefreshToken(response._id);
// 2. Lưu Refresh Token vào Database để sau này còn đối chiếu
await User.findByIdAndUpdate(response._id, { refreshToken }, { new: true });
// 3. Đỉnh cao bảo mật: Nhét Refresh Token vào Cookie kèm cờ httpOnly
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000 // Sống đúng 7 ngày
});
const { password: _, role, ...userData } = response.toObject();
// 4. Trả Access Token thẳng về client (Frontend sẽ tự lưu vào Redux/LocalStorage)
return res.status(200).json({
success: true,
accessToken,
userData,
});
});
Bí kíp thực chiến: Anh em thấy mình nhét refreshToken vào Cookie với cờ httpOnly: true không? Đây là "tấm khiên" cực mạnh chống lại lỗi XSS. Bất cứ đoạn code Javascript nào ở Frontend cũng không thể đọc được cái cookie này, chỉ có trình duyệt mới có quyền tự động đính kèm nó khi gửi request lên server thôi. Hacker có hack được Frontend cũng mếu máo quay về!
3. Trạm gác an ninh: Verify Access Token
Thẻ cấp rồi, giờ phải làm trạm gác để kiểm tra. Tạo file middlewares/verifyToken.js:
const jwt = require('jsonwebtoken');
const asyncHandler = require('express-async-handler');
const verifyAccessToken = asyncHandler(async (req, res, next) => {
// Client phải gửi token trong header Authorization với prefix là 'Bearer '
if (req?.headers?.authorization?.startsWith('Bearer')) {
const token = req.headers.authorization.split(' ')[1]; // Tách chữ Bearer ra để lấy cái mã
// Dùng con dấu JWT_SECRET để check xem thẻ giả hay thật, còn hạn hay hết
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(401).json({ success: false, message: 'Invalid access token' });
}
// Nếu thẻ xịn, lưu thông tin giải mã (chứa _id và role) vào req để các bước sau dùng
req.user = decoded;
next();
});
} else {
return res.status(401).json({ success: false, message: 'Require authentication!!!' });
}
});
module.exports = { verifyAccessToken };
4. Ứng dụng thực tế: API lấy thông tin cá nhân (getCurrent)
Có trạm gác rồi, mình thử làm một API yêu cầu phải đăng nhập mới xem được nhé (vào controllers/user.js viết thêm hàm này):
const getCurrent = asyncHandler(async (req, res) => {
// Cái req.user._id này có được là nhờ cái chốt chặn verifyAccessToken ở trên gán vào đó
const { _id } = req.user;
// Dấu trừ đằng trước nghĩa là "lấy hết thông tin NHƯNG trừ mấy trường này ra"
const user = await User.findById(_id).select('-refreshToken -password -role');
if (!user) return res.status(404).json({ success: false, message: 'User not found' });
return res.status(200).json({ success: true, user });
});
Sau đó anh em cập nhật lại filerouters/user.js:
const router = require('express').Router();
const ctrls = require('../controllers/user');
const { verifyAccessToken } = require('../middlewares/verifyToken');
router.post('/login', ctrls.login);
// Muốn chạy vào getCurrent, phải đi qua chốt verifyAccessToken
router.get('/current', verifyAccessToken, ctrls.getCurrent);
module.exports = router;
5. Khai báo thư viện đọc Cookie ở Server
Vì lúc nãy mình có xài tính năng gán Cookie, nên ở server.js anh em nhớ gọi thư viện cookie-parser ra dùng nhé (chưa cài thì nhớ gõ npm i cookie-parser):
const express = require('express');
const cookieParser = require('cookie-parser'); // Thêm dòng này
const userRoutes = require('./routers/user');
const app = express();
app.use(express.json());
app.use(cookieParser()); // Bật tính năng đọc hiểu Cookie
app.use('/api/users', userRoutes);
app.listen(process.env.PORT || 3000, () => {
console.log('Server is running...');
});
Lời kết
Xong! Bật Postman lên test thử nào. Bắn API Login, anh em sẽ nhận về một cục accessToken và thấy trong tab Cookies có cái refreshToken nằm chễm chệ ở đó. Lấy cái accessToken đó, set vào tab Authorization > Bearer Token, bắn tiếp vào API /current, server sẽ trả về đúng thông tin của anh em. Cảm giác phê chữ ê kéo dài luôn!
Nhưng khoan đã... Access Token của mình set có 3 ngày là hết hạn. Sau 3 ngày, verifyAccessToken sẽ văng lỗi Invalid access token. Lúc đó user đang lướt app lại bị văng ra bắt đăng nhập lại à? Trải nghiệm vậy là hỏng bét!
Đó là lý do chúng ta có file dự kiến cho bài sau: Lession 6: Access token và refresh token P2. Mình sẽ hướng dẫn anh em dùng cái refreshToken (sống tận 7 ngày) để âm thầm đi xin một cái accessToken mới toanh, giúp user lướt app mượt mà không bao giờ bị văng.
All rights reserved