Hướng Dẫn Xây Dựng Token Refresh JWT Trong Node.js
Khi xây dựng hệ thống xác thực trong ứng dụng Node.js, JSON Web Tokens (JWT) là một trong những phương pháp phổ biến nhất. Chúng nhanh chóng, không trạng thái và dễ sử dụng. Tuy nhiên, chúng cũng có một điểm yếu: thời gian hết hạn. Nếu bạn phát hành một token ngắn hạn (ví dụ: 15 phút), người dùng sẽ thường xuyên bị đăng xuất. Ngược lại, nếu phát hành token dài hạn (ví dụ: 7 ngày), bạn sẽ tăng rủi ro bảo mật nếu token bị lộ.
Đó là lý do tại sao refresh tokens xuất hiện. Trong bài viết này, chúng ta sẽ phân tích cách hoạt động của JWT và refresh tokens, lý do bạn cần chúng, và cách triển khai một chiến lược refresh token an toàn trong Node.js.
🔑 Refresh Tokens Là Gì?
JWT access token được sử dụng để xác thực các yêu cầu. Nó có thời gian sống ngắn, thường là từ 10 đến 30 phút, để giảm thiểu rủi ro.
Refresh token là một thông tin đăng nhập dài hạn (có thể là vài ngày hoặc vài tuần) cho phép khách hàng yêu cầu một access token mới mà không cần đăng nhập lại.
- Access Token → Nhanh, không trạng thái, hết hạn nhanh.
- Refresh Token → Dài hạn, được lưu trữ an toàn, chỉ được sử dụng để gia hạn access tokens.
🛠️ Thiết Lập Dự Án
Hãy bắt đầu với một ứng dụng Express.js tối giản với xác thực JWT và refresh tokens.
1. Cài Đặt Các Thư Viện
bash
npm init -y
npm install express jsonwebtoken bcryptjs cookie-parser dotenv
express→ Framework webjsonwebtoken→ Để ký/ xác minh JWTbcryptjs→ Để mã hóa mật khẩucookie-parser→ Để xử lý cookies (nơi lưu trữ refresh tokens)dotenv→ Quản lý biến môi trường
⚙️ Thiết Lập Server Cơ Bản
javascript
// server.js
const express = require("express");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const cookieParser = require("cookie-parser");
require("dotenv").config();
const app = express();
app.use(express.json());
app.use(cookieParser());
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server đang chạy trên ${PORT}`));
🔐 Tạo Tokens
Chúng ta sẽ cần hai hàm tạo token:
generateAccessToken(user)→ ngắn hạn (ví dụ: 15m)generateRefreshToken(user)→ dài hạn (ví dụ: 7d)
javascript
function generateAccessToken(user) {
return jwt.sign({ id: user.id, email: user.email }, process.env.ACCESS_SECRET, {
expiresIn: "15m",
});
}
function generateRefreshToken(user) {
return jwt.sign({ id: user.id, email: user.email }, process.env.REFRESH_SECRET, {
expiresIn: "7d",
});
}
👤 Đường Dẫn Đăng Nhập
Khi người dùng đăng nhập, chúng ta phát hành cả hai token:
javascript
const users = []; // cơ sở dữ liệu giả
app.post("/register", async (req, res) => {
const { email, password } = req.body;
const hashed = await bcrypt.hash(password, 10);
users.push({ id: users.length + 1, email, password: hashed });
res.json({ message: "Người dùng đã được đăng ký" });
});
app.post("/login", async (req, res) => {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user) return res.status(400).json({ error: "Người dùng không tồn tại" });
const valid = await bcrypt.compare(password, user.password);
if (!valid) return res.status(400).json({ error: "Mật khẩu không hợp lệ" });
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// Lưu refresh token vào cookie HttpOnly
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: true, // bật trong môi trường sản xuất (https)
sameSite: "strict",
});
res.json({ accessToken });
});
🔄 Làm Mới Tokens
Chúng ta sẽ thêm một endpoint /refresh để phát hành một access token mới khi token cũ hết hạn.
javascript
app.post("/refresh", (req, res) => {
const token = req.cookies.refreshToken;
if (!token) return res.status(401).json({ error: "Không có token" });
jwt.verify(token, process.env.REFRESH_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: "Token không hợp lệ" });
const newAccessToken = generateAccessToken(user);
res.json({ accessToken: newAccessToken });
});
});
🔒 Middleware Để Bảo Vệ Các Đường Dẫn
javascript
function authMiddleware(req, res, next) {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, process.env.ACCESS_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
app.get("/protected", authMiddleware, (req, res) => {
res.json({ message: "Bạn đã được ủy quyền!", user: req.user });
});
🚪 Đăng Xuất
Để đăng xuất, chỉ cần xóa refresh token.
javascript
app.post("/logout", (req, res) => {
res.clearCookie("refreshToken");
res.json({ message: "Đăng xuất thành công" });
});
⚠️ Thực Hành Bảo Mật Tốt Nhất
- Lưu access token trong bộ nhớ (không phải localStorage/sessionStorage).
- Lưu refresh token trong cookie HttpOnly → bảo vệ khỏi XSS.
- Xoay vòng refresh tokens → phát hành một refresh token mới mỗi khi nó được sử dụng.
- Đen danh các refresh tokens cũ nếu bạn muốn kiểm soát hoàn toàn (lưu trữ chúng trong Redis hoặc cơ sở dữ liệu).
- Sử dụng HTTPS trong môi trường sản xuất để ngăn chặn rò rỉ token.
🔄 Làm Mới Tokens - Phân Tích
Dưới đây là cách mà nó tương ứng với các yêu cầu frontend-backend:
-
Đăng Nhập:
- Frontend gửi tên đăng nhập/mật khẩu.
- Backend kiểm tra thông tin và cung cấp cho bạn:
- Một access token ngắn hạn (vé xem phim)
- Một refresh token dài hạn (thẻ mùa, lưu trữ trong cookie HttpOnly).
-
Truy Cập các đường dẫn được bảo vệ:
- Frontend đính kèm access token trong tiêu đề yêu cầu.
- Backend xác minh nó. Nếu hợp lệ → cho phép truy cập.
-
Access token hết hạn:
- Frontend cố gắng thực hiện yêu cầu → nhận được
401 Unauthorized. - Frontend sau đó gọi
/refreshmà không gửi tên đăng nhập/mật khẩu, chỉ cho phép trình duyệt tự động gửi cookie refresh token.
- Frontend cố gắng thực hiện yêu cầu → nhận được
-
Backend xác minh refresh token:
- Nếu hợp lệ → phát hành một access token mới (vé xem phim mới).
- Nếu không hợp lệ/hết hạn → người dùng phải đăng nhập lại (thẻ mùa đã hết hạn).
Điểm chính:
Frontend không bao giờ có quyền truy cập trực tiếp vào refresh token nếu bạn lưu nó trong cookie HttpOnly. Nó chỉ “tin tưởng trình duyệt” để gửi nó cùng với các yêu cầu /refresh. Điều này giữ cho nó an toàn khỏi các kịch bản độc hại.
🎯 Kết Luận
Việc sử dụng refresh tokens với JWTs cân bằng bảo mật và khả năng sử dụng. Bạn có được access tokens ngắn hạn để bảo vệ và refresh tokens dài hạn để tiện lợi. Trong môi trường sản xuất, luôn phải xử lý việc lưu trữ refresh token cẩn thận, triển khai quay vòng, và kết hợp với HTTPS để tạo ra một hệ thống an toàn.
Cấu trúc này có thể mở rộng, hoạt động tốt với các dịch vụ không trạng thái và được sử dụng rộng rãi trong các hệ thống xác thực hiện đại.