Trong các ứng dụng web hiện đại, việc xác thực và ủy quyền người dùng là yếu tố quan trọng để bảo đảm an ninh. JWT (JSON Web Token) đã trở thành một trong những công nghệ phổ biến nhất để xử lý xác thực, nhờ tính gọn nhẹ và khả năng hoạt động tốt trong các hệ thống phân tán. Bài viết này sẽ giải thích chi tiết và tỉ mỉ về cách JWT hoạt động trong quy trình xác thực người dùng, từ việc đăng nhập, xử lý yêu cầu đến làm mới token.
JWT là gì?
JWT là viết tắt của JSON Web Token, một tiêu chuẩn mở (RFC 7519) để truyền tải dữ liệu an toàn giữa hai bên dưới dạng JSON. Token này được mã hóa và có thể xác thực tính toàn vẹn của nó thông qua chữ ký số. Trong một hệ thống xác thực, JWT được sử dụng để định danh và cấp quyền cho người dùng, giúp duy trì phiên làm việc mà không cần lưu trữ trạng thái trên máy chủ.
Cấu trúc của JWT
JWT có ba phần chính:
- Header: Chứa thông tin về loại token và thuật toán mã hóa.
- Payload: Chứa thông tin người dùng hoặc các thông tin mà bạn muốn truyền tải.
- Signature: Được tạo bằng cách mã hóa header và payload cùng với một khóa bí mật.
Một JWT thường có dạng như sau:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Luồng xử lý Authentication với JWT
Dưới đây là luồng chi tiết về cách JWT hoạt động trong quá trình xác thực người dùng:
1. Đăng nhập và tạo JWT
Gửi yêu cầu đăng nhập
Khi người dùng nhập thông tin đăng nhập (như email và mật khẩu) và gửi chúng đến server qua HTTP POST, server sẽ kiểm tra thông tin đó.
Ví dụ:
POST /login
Content-Type: application/json
{
"email": "[email protected]",
"password": "password123"
}
Xác thực thông tin đăng nhập
Server nhận thông tin và xác thực bằng cách so sánh với dữ liệu lưu trữ trong cơ sở dữ liệu. Nếu thông tin hợp lệ, server sẽ tạo một JWT và gửi lại cho người dùng.
Tạo JWT
JWT được tạo với một payload chứa các thông tin người dùng như userId
, roles
, và thời gian hiệu lực (exp
). Header của JWT xác định thuật toán mã hóa, thường là HMAC SHA256.
Ví dụ:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022,
"exp": 1516249022
}
Phần chữ ký (Signature) sẽ được mã hóa bằng cách sử dụng khóa bí mật để bảo đảm tính toàn vẹn của token.
Trả về JWT
JWT sau khi tạo xong sẽ được gửi trả lại cho client trong HTTP response. Người dùng sẽ lưu trữ JWT này để sử dụng cho các yêu cầu tiếp theo.
Ví dụ:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
2. Lưu trữ JWT trên Client
JWT có thể được lưu trữ ở nhiều vị trí trên client, tùy thuộc vào mục tiêu bảo mật và yêu cầu của ứng dụng:
- LocalStorage: Lưu trữ bền vững trong trình duyệt, tiện lợi nhưng dễ bị tấn công XSS.
- SessionStorage: Lưu trong phiên trình duyệt, ít bền vững hơn nhưng an toàn hơn so với LocalStorage.
- Cookies: Lưu trữ dưới dạng cookie, có thể thêm các thuộc tính bảo mật như
HttpOnly
và Secure
.
Ví dụ lưu trữ JWT trong LocalStorage:
localStorage.setItem("token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...");
3. Gửi JWT trong các yêu cầu tiếp theo
Trong các yêu cầu tiếp theo, client cần gửi JWT đã nhận để xác thực. Token này thường được gửi trong HTTP header Authorization
dưới dạng Bearer Token.
Ví dụ yêu cầu với JWT:
GET /protected-resource
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT sẽ được gửi cùng mọi yêu cầu tới các API bảo mật. Server sau đó sẽ xác thực token này để cấp quyền truy cập.
4. Xác thực JWT trên Server
Xác thực chữ ký
Khi server nhận được yêu cầu có chứa JWT, nó sẽ xác thực chữ ký để đảm bảo rằng token chưa bị thay đổi. Việc xác thực dựa vào khóa bí mật đã được sử dụng để mã hóa JWT.
Giải mã JWT
Nếu token hợp lệ, server sẽ giải mã payload và trích xuất thông tin người dùng từ đó. Thông tin này có thể bao gồm userId
, quyền hạn của người dùng (roles), và thời gian hết hạn của token (exp).
Xác thực thời hạn token
Server kiểm tra xem JWT có còn hợp lệ không dựa trên thuộc tính exp
. Nếu token đã hết hạn, server sẽ trả về lỗi yêu cầu đăng nhập lại (thường là 401 Unauthorized).
Ví dụ kiểm tra token trong ứng dụng Node.js:
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const token = req.header('Authorization').split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
5. Cấp quyền truy cập (Authorization)
Khi xác thực thành công JWT, server có thể sử dụng thông tin từ payload để xác định quyền truy cập. Nếu người dùng có quyền hợp lệ, server sẽ cho phép truy cập tài nguyên. Nếu không, server trả về mã lỗi như 403 Forbidden.
Ví dụ kiểm tra quyền trong hệ thống:
function authorizeAdmin(req, res, next) {
if (!req.user.admin) return res.sendStatus(403);
next();
}
6. Làm mới JWT (Refresh Token)
Sử dụng Refresh Token
JWT thường có thời gian sống ngắn, do đó để tránh việc người dùng phải đăng nhập lại liên tục, ta sử dụng Refresh Token. Refresh Token có thời gian sống lâu hơn JWT và được sử dụng để cấp mới JWT khi nó hết hạn.
Ví dụ luồng xử lý làm mới token:
- Client gửi Refresh Token đến server khi JWT hết hạn.
- Server xác minh Refresh Token và nếu hợp lệ, tạo một JWT mới.
- Client nhận JWT mới và thay thế JWT cũ trong các yêu cầu tiếp theo.
app.post('/token', (req, res) => {
const refreshToken = req.body.token;
if (!refreshToken) return res.sendStatus(401);
if (!refreshTokens.includes(refreshToken)) return res.sendStatus(403);
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
const accessToken = generateAccessToken({ name: user.name });
res.json({ accessToken });
});
});
Lưu ý: Refresh Token thường được lưu trữ trong Cookies để bảo đảm an toàn.
JWT cung cấp một phương pháp xác thực an toàn, không trạng thái và rất hiệu quả cho các ứng dụng web hiện đại. Tuy nhiên, việc bảo mật JWT rất quan trọng, và bạn cần cân nhắc việc bảo vệ token trước các cuộc tấn công tiềm ẩn như XSS, CSRF. Trong quá trình phát triển, hãy luôn kiểm tra thời hạn và quyền hạn của token để bảo đảm rằng hệ thống của bạn luôn an toàn.