
Trong kỷ nguyên của các ứng dụng Single Page Application (React, Vue, Angular) và Mobile App, JWT (JSON Web Token) đã trở thành chuẩn mực quốc tế trong việc xác thực người dùng (Authentication).
Tuy nhiên, lỗi phổ biến nhất của các bạn Junior Developer khi làm việc với JWT là: Chỉ tạo ra duy nhất một Access Token có thời hạn cực dài (ví dụ: 30 ngày) để người dùng không phải đăng nhập lại.
Đây là một “lỗ hổng chí mạng”! Nếu Access Token đó bị kẻ xấu đánh cắp thông qua các cuộc tấn công XSS hoặc đánh chặn qua mạng, chúng sẽ có toàn quyền thao túng tài khoản của user suốt 30 ngày đó mà bạn không có cách nào thu hồi được.
Bài viết này sẽ hướng dẫn anh em NCC cách khắc phục triệt để bằng chiến lược Refresh Token chuẩn chỉnh.
Bản Chất Của Vấn Đề: Tại Sao Phải Cần Đến 2 Loại Token?
Để vừa đảm bảo an toàn cho hệ thống, vừa không làm phiền người dùng bắt họ đăng nhập liên tục sau mỗi 5-10 phút, chúng ta cần chia cơ chế xác thực thành 2 loại Token riêng biệt:
- Access Token:
- Nhiệm vụ: Dùng để đính kèm vào Header của mỗi request nhằm truy cập vào các API được bảo mật.
- Thời gian sống (TTL): Cực ngắn (chỉ từ 5 phút đến 15 phút).
- Lưu trữ: Thường lưu ở bộ nhớ tạm (Memory/State) của Frontend.
- Refresh Token:
- Nhiệm vụ: Duy nhất một việc – Đổi lấy một Access Token mới khi cái cũ hết hạn.
- Thời gian sống (TTL): Dài (từ 7 ngày đến 30 ngày).
- Lưu trữ: Lưu ở Database phía Backend và lưu ở
HttpOnly Cookiephía Frontend để chống tấn công XSS.
Kịch Bản Vận Hành Của Cơ Chế Refresh Token
Hãy hình dung luồng đi của dữ liệu thông qua bảng quy trình thực tế dưới đây:
| Bước | Phía Client (Frontend) | Phía Server (Backend) |
| 1. Đăng nhập | Gửi Username/Password. | Xác thực đúng $\rightarrow$ Tạo cả Access Token (hạn 10p) và Refresh Token (hạn 7 ngày). Lưu Refresh Token vào DB. Trả cả 2 về Client. |
| 2. Gọi API thường | Đính kèm Access Token vào Header Authorization: Bearer <token>. | Giải mã Access Token $\rightarrow$ Hợp lệ $\rightarrow$ Trả dữ liệu về cho Client. |
| 3. Token hết hạn | Gửi request gọi API nhưng nhận về lỗi 401 Unauthorized (do Access Token quá 10 phút). | Trả về lỗi 401 báo hiệu Token đã hết hạn. |
| 4. Kịch bản Refresh | Âm thầm (Silent) gọi một API đặc biệt: /api/refresh-token, đính kèm Refresh Token đang giữ. | Kiểm tra Refresh Token trong DB và tính hợp lệ $\rightarrow$ Nếu OK, tạo một Access Token MỚI trả về cho Client. |
| 5. Tiếp tục | Lưu Access Token mới và tự động gọi lại API bị lỗi ở Bước 3. Người dùng không hề nhận ra sự gián đoạn nào! | Phản hồi dữ liệu như bình thường. |
Hướng Dẫn Viết Code Triển Khai Thực Chiến Với Node.js
Hãy cùng hiện thực hóa lý thuyết trên bằng một đoạn code Backend Node.js ngắn gọn, súc tích sử dụng thư viện jsonwebtoken.
Bước 1: Hàm tạo Access Token và Refresh Token
JavaScript
const jwt = require('jsonwebtoken');
const ACCESS_TOKEN_SECRET = "NCC_SUPER_SECRET_KEY_123";
const REFRESH_TOKEN_SECRET = "NCC_SUPER_REFRESH_KEY_999";
// Tạo Access Token hạn ngắn
const generateAccessToken = (user) => {
return jwt.sign({ id: user.id, role: user.role }, ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
};
// Tạo Refresh Token hạn dài
const generateRefreshToken = (user) => {
return jwt.sign({ id: user.id }, REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
};
Bước 2: API Endpoint xử lý việc cấp đổi Token khi Access Token hết hạn
JavaScript
// Giả lập Database lưu trữ các Refresh Token đang hoạt động hợp lệ
let refreshTokensDatabase = [];
app.post('/api/refresh-token', (req, res) => {
// Lấy Refresh Token từ Cookie hoặc Body
const refreshToken = req.cookies.refreshToken || req.body.refreshToken;
if (!refreshToken) return res.status(401).json("Bạn chưa xác thực!");
// Kiểm tra xem token này có tồn tại trong DB không (tránh token lậu hoặc đã bị thu hồi)
if (!refreshTokensDatabase.includes(refreshToken)) {
return res.status(403).json("Refresh Token không hợp lệ hoặc đã bị thu hồi!");
}
// Xác thực chữ ký của Refresh Token
jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, user) => {
if (err) return res.status(403).json("Token đã hết hạn hoặc bị sửa đổi!");
// Nếu mọi thứ đều ổn -> Cấp một Access Token mới tinh
const newAccessToken = generateAccessToken({ id: user.id, role: user.role });
res.json({
accessToken: newAccessToken
});
});
});
Giải Pháp Nâng Cao: Cơ Chế Thu Hồi Quyền Lực (Token Revocation)
Một trong những ưu điểm lớn nhất của việc lưu Refresh Token vào Database là bạn có khả năng Thu hồi quyền truy cập.
Nếu một người dùng báo mất máy điện thoại, hoặc Tech Lead NCC phát hiện một tài khoản có dấu hiệu phá hoại hệ thống, Backend chỉ cần chạy một lệnh xóa sạch các Refresh Token của User đó trong Database:
JavaScript
// Câu lệnh giả lập thu hồi toàn bộ phiên đăng nhập của User cụ thể
refreshTokensDatabase = refreshTokensDatabase.filter(token => token.userId !== targetUserId);
Ngay lập tức, ở lần gia hạn tiếp theo (tối đa là 15 phút sau khi Access Token cũ hết hạn), kẻ tấn công sẽ hoàn toàn bị đá văng ra khỏi hệ thống vì lệnh /api/refresh-token sẽ trả về lỗi 403 Forbidden. Đây là điều mà nếu chỉ dùng JWT đơn thuần không bao giờ làm được!
Kết Luận và Takeaway
Bảo mật là một quá trình đánh đổi giữa sự tiện lợi và tính an toàn. Chiến lược Refresh Token chính là điểm giao thoa hoàn hảo giúp ứng dụng của bạn đạt chứng chỉ bảo mật cao mà không gây khó chịu cho người dùng cuối.
Tương tự như việc bạn áp dụng Docker Multi-stage Build để dọn dẹp bộ nhớ Image, việc thiết lập một kiến trúc Authentication phân tầng rõ ràng sẽ giúp hệ thống của dự án phát triển bền vững và an toàn tuyệt đối.
Takeaway: Luôn giữ nguyên tắc: Access Token sống ngắn + Lưu Memory, Refresh Token sống dài + Lưu HttpOnly Cookie + Có lưu vết ở Database.
