Bài viết này sẽ giúp bạn hiểu về cơ chế hoạt động, các lỗi sai thường thấy khi sử dụng JWT. Từ đó, giúp bạn áp dụng công nghệ này tốt, hiệu quả hơn.
1. Cơ chế hoạt động của JWT
- Client gửi yêu cầu đăng nhập với thông tin xác thực.
- Server xác thực thông tin và tạo JWT token với secret key, sau đó gửi lại cho client.
- Client sẽ lưu trữ JWT token (thường là trong localStorage hoặc sessionStorage).
- Từ giai đoạn này, tất cả các request client gửi lên server sẽ đính kèm JWT token.
- Server kiểm tra tính hợp lệ của JWT token bằng cách lấy phần Signature (chữ ký) bên trong token, verify xem chữ ký nhận được chính xác là được hash cùng thuật toán và chuỗi secret key hay không.
- Tùy vào từng trường hợp mà server sẽ phản hồi về cho client.
2. Khi nào nên sử dụng JWT
- Web và Mobile Application: Dễ dàng tích hợp để xác thực người dùng và trao đổi thông tin an toàn giữa ứng dụng và server.
- Trong kiến trúc Microservices: Trao đổi thông tin xác thực dễ dàng, nhanh chóng, bảo mật giữa các service với nhau.
- API Authentication: Sử dụng để xác thực và phân quyền đảm bảo chỉ người dùng hợp lệ mới có thể truy cập.
3. JWT có thực sự bảo vệ dữ liệu của chúng ta?
- JWT hoàn toàn không che giấu, ẩn hay làm mờ bất kì dữ liệu nào cả.
- Các bạn có thể xem lại cấu trúc JWT. Dữ liệu chỉ được Encoded và Hash chứ không phải Encrypted.
- Nếu bạn còn đang nhầm lẫn rằng Encoded và Encrypted giống nhau thì hãy tìm hiểu thêm tại đây.
Vì thế, nếu một người muốn tấn công bằng phương pháp Man-in-the-middle bắt được gói tin có chứa token, chỉ cần decode ra thì sẽ lấy được thông tin của người dùng. Hãy luôn luôn đảm bảo ứng dụng của bạn chắc chắn phải có giao thức mã hóa đường truyền HTTPS nhé.
4. Những lỗi sai thường gặp khi sử dụng JWT
Một vài lỗi sai thường gặp bạn nên lưu ý khi sử dụng JWT trong dự án để đảm bảo dự án tăng tính bảo mật và hiệu quả.
- Sử dụng secret key yếu, dễ dự đoán: dễ dàng bị tấn công và giải mã dễ dàng.
- Lưu trữ token không an toàn: Lưu trữ trong localStorage hay sessionStorage có thể dễ bị tấn công XSS (Cross-Site Scripting).
- Không thiết lập thời gian hết hạn: Token sẽ luôn tồn tại vĩnh viễn nếu bạn không thiết lập thời gian hết hạn cho token, nguy cơ về bảo mật nếu như token bị lộ.
- Truyền qua kết nối không an toàn: Token dễ bị đánh cắp, luôn sử dụng HTTPs để truyền tải các dữ liệu nhạy cảm.
5. Hướng dẫn sử dụng JWT với NodeJS
Để sử dụng JWT với NodeJS, bạn sẽ sử dụng các thư viện phổ biến như jsonwebtoken và express.
5.1 Khởi tạo dự án NodeJS
- Mở terminal và tạo thư mục chứa project:
cd jwt-example
- Khởi tạo và cài đặt thư viện:
Sau khi chạy xong, file package.json được tạo, bắt đầu cài đặt thư viện
5.2 Cấu trúc thư mục
5.3 Code thôi nào
- src/server.js
const express = require('express');
const apiRoutes = require('./routes/api');
const app = express();
app.use(express.json());
app.use('/api', apiRoutes);
app.listen(3000, () => {
console.log('200lab server running on 3000');
});
- src/routes/api.js
Ngay tại dòng lấy danh sách khóa học thì api này sẽ phải chạy qua authMiddleware để kiểm tra xác thực cái token hợp lệ thì mới cho đi xử lý tiếp sang bên courseController.
const express = require('express');
const authController = require('../controllers/AuthController');
const courseController = require('../controllers/courseController');
const authMiddleware = require('../middlewares/authMiddleware');
const router = express.Router();
router.post('/login', authController.login);
router.post('/refresh-token', authController.refreshToken);
router.get('/course', authMiddleware.authenticateJWT, courseController.getCourseData);
module.exports = router;
- src/middlewares/authMiddleware.js
Hàm này dùng để xác thực JSON Web Token (JWT) đảm bảo rằng chỉ những request đính với JWT hợp lệ mới tiếp tục xử lý.
const jwtHelper = require('../helpers/jwt');
exports.authenticateJWT = (req, res, next) => {
const authHeader = req.header('Authorization');
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.sendStatus(401);
}
try {
const user = jwtHelper.verifyAccessToken(token);
req.user = user;
next();
} catch (error) {
res.sendStatus(403);
}
};
- src/utils/generateJti.js
function generateUniqueJTI() {
const timestamp = Date.now().toString();
const random = Math.random().toString(36).substring(2, 15);
return timestamp + random;
}
exports.generateUniqueJTI = generateUniqueJTI;
- src/helpes/jwt.js
const jwt = require('jsonwebtoken');
const { generateUniqueJTI } = require('../utils/generateJti');
const secretKey = process.env.SECRET_KEY || '200lab-server-level-up-your-coding-skills';
const refreshTokenSecret = process.env.REFRESH_KEY || '200lab-server-level-up-your-coding-skills';
exports.generateAccessToken = (user) => {
let payload = {
iss: '200lab.io',
sub: user.userId,
aud: user.username,
exp: Math.floor(Date.now() / 1000) + (15 * 60),
iat: Math.floor(Date.now() / 1000),
nbf: Math.floor(Date.now() / 1000) + 10,
jti: generateUniqueJTI(),
};
const token = jwt.sign(payload, secretKey, { algorithm: "HS256" });
return token;
};
exports.generateRefreshToken = (user) => {
let payload = {
iss: '200lab.io',
sub: user.userId,
aud: user.username,
exp: Math.floor(Date.now() / 1000) + (15 * 60),
iat: Math.floor(Date.now() / 1000),
nbf: Math.floor(Date.now() / 1000) + 10,
jti: generateUniqueJTI(),
};
return jwt.sign(payload, refreshTokenSecret, { algorithm: "HS256" });
};
exports.verifyAccessToken = (token) => {
return jwt.verify(token, secretKey);
};
exports.verifyRefreshToken = (token) => {
return jwt.verify(token, refreshTokenSecret);
};
Các loại key bạn nên lưu tại file .env, không để lộ ra ngoài.
Khi ký token, phần khai báo thuật toán algorithm: "HS256" có thể không cần khai báo mặc định là HS256. Các bạn có thể dùng thuật toán khác như: PS256, HS512, ES256, RS256,...
Lưu ý nhỏ khi tạo chữ ký khuyên bạn NÊN chứa đầy đủ các trường sau:
- iss (issuer): Người/hệ thống phát hành JWT.
- sub (subject): Chủ thể của JWT, xác định rằng đây là người sở hữu hoặc có quyền truy cập resource (tài nguyên).
- aud (audience): Người nhận mà JWT hướng tới.
- exp (expiration time): Đậy là thời điểm JWT sẽ bị vô hiệu hóa.
- nbf (not before time): JWT chỉ có hiệu lực sau thời gian này.
- iat (issued at time): Thời gian JWT được tạo ra.
- jti (JWT ID): Là mã duy nhất; có thể dùng để ngăn JWT bị tái sử dụng (cho phép một token chỉ được sử dụng một lần).
- src/controllers/authController.js
Mình tạo ra danh sách giả định có 2 tài khoản dùng để đăng nhập. Nếu đúng thông tin sẽ tạo accesstoken cùng với refreshToken và cấp mới lại accessToken.
const jwtHelper = require('../helpers/jwt');
const users = [
{ id: 1, username: 'user1', password: 'password1' },
{ id: 2, username: 'user2', password: 'password2' }
];
exports.login = (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username && u.password === password);
if (user) {
const accessToken = jwtHelper.generateAccessToken({ userId: user.id, username: user.username });
const refreshToken = jwtHelper.generateRefreshToken({ userId: user.id, username: user.username });
res.json({ accessToken, refreshToken });
} else {
res.status(401).send('Invalid credentials');
}
};
exports.refreshToken = (req, res) => {
const { token } = req.body;
if (!token) {
return res.sendStatus(401);
}
try {
const user = jwtHelper.verifyRefreshToken(token);
const newAccessToken = jwtHelper.generateAccessToken({ userId: user.userId, username: user.username });
res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(403).send('Invalid refresh token');
}
};
- src/controllers/courseController.js
Trong file này mình viết một controller đơn giản lấy ra danh sách một vài khóa học nổi bật của 200lab.
Đây sẽ là controller mà sau khi xác thực người dùng thành công thì mới cho phép lấy thông tin rồi trả về kết quả.
exports.getCourseData = (req, res) => {
const fakeList = [
{ id: 1, name: 'Khóa học Design System - Thiết kế hệ thống Microservice', description: 'Mentor: Việt Trần' },
{ id: 2, name: 'Khóa học DevOps Azure Cloud', description: 'Mentor: Trần Minh Nhật' },
{ id: 3, name: 'Tiếng Anh Giao Tiếp Dành Cho Dev', description: 'Thầy Nhiên Trần' },
];
res.json(fakeList);
};
5.4 Cùng xem kết quả đoạn code
Để trực quan thì mình sẽ chạy ứng dụng lên và dùng Postman để chạy các API vừa code phía trên.
Khi chạy ứng dụng lên thì đây là phần hiển thị tại terminal
5.4.1 POST: /api/login
Nếu bạn muốn biết phần payload của JWT từ result trả về, bạn có thể coppy accessToken và paste vào đây để xem.
5.4.2 GET: /api/cource
Trong trường hợp mình truyền token không hợp lệ hoặc không truyền token thì kết quả sẽ như thế này:
5.4.3 POST: /api/refresh-token
Khi accessToken hết hạn thì phía client gọi api POST: /api/refresh-token để làm mới lại accessToken.
6. Kết luận
Thông qua bài viết này bạn sẽ:
- Hiểu rõ hơn về cơ chế hoạt động của JWT, từ quá trình tạo, xác thực token cho đến cách mã hóa và giải mã.
- Nhận biết được những trường hợp nên sử dụng JWT.
- Nhận diện một vài lỗi phổ biến khi triển khai JWT từ đó sử dụng JWT một cách an toàn, hiệu quả, giảm thiểu rủi ro và bảo vệ dữ liệu.
- Hướng dẫn chi tiết về cách tích hợp JWT vào dự án NodeJS.
Nếu yêu thích và quan tâm các chủ đề khác về lập trình, bạn hãy thường xuyên theo dõi các bài viết hay về Lập Trình & Dữ Liệu trên 200Lab Blog nhé.