JWT vs Session-Based Authentication: Đâu là lựa chọn phù hợp
23 Jun, 2025
Hướng nội
AuthorBài viết này sẽ phân tích hai giải pháp phổ biến để duy trì trạng thái đăng nhập là Session-Cookie và JSON Web Token (JWT)

Mục Lục
Khi phát triển ứng dụng web, một trong những thách thức cơ bản nhất là làm sao để duy trì trạng thái đăng nhập cho người dùng. Bởi lẽ, giao thức HTTP vốn được thiết kế theo kiểu "stateless" (không lưu trạng thái), tức là mỗi lần client gửi yêu cầu lên server đều được xem như một giao dịch hoàn toàn độc lập, không liên kết gì đến các lần trước đó. Để khắc phục điều này, các kỹ sư đã nghĩ ra nhiều cơ chế giúp "ghi nhớ" người dùng, trong đó hai phương pháp được sử dụng rộng rãi nhất hiện nay là Session-Cookie và JSON Web Token (JWT).
Bài viết này sẽ đi sâu phân tích cách thức hoạt động, điểm mạnh - điểm yếu, cùng các trường hợp sử dụng điển hình của từng phương pháp, bên cạnh đó là các ví dụ minh họa thực tế bằng TypeScript.
1. Session-Based Authentication (Stateful)
Đây là cơ chế xác thực phía server (server-side). Hãy tưởng tượng bạn đến một thư viện, đăng ký và nhận một thẻ thành viên, thư viện sẽ lưu trữ hồ sơ của bạn. Mỗi lần bạn quay lại, bạn chỉ cần xuất trình thẻ thành viên, thủ thư sẽ tra cứu hồ sơ để xác nhận danh tính của bạn.
Cơ chế Session hoạt động tương tự: server tạo và duy trì một "phiên" cho mỗi người dùng đã được xác thực.
Luồng Hoạt Động:
- (Client → Server) Đăng nhập: Người dùng gửi thông tin xác thực (ví dụ: email và mật khẩu) đến một endpoint của server (ví dụ:
/login
). - (Server) Xác thực và Tạo Session: Server kiểm tra thông tin. Nếu hợp lệ, server sẽ:
- Tạo một record session duy nhất trong kho lưu trữ của nó (có thể là bộ nhớ, database, hoặc một cache layer như Redis). Bản ghi này chứa thông tin định danh người dùng (ví dụ:
userId
). - Tạo một
Session ID
ngẫu nhiên, an toàn để liên kết với bản ghi session vừa tạo.
- Tạo một record session duy nhất trong kho lưu trữ của nó (có thể là bộ nhớ, database, hoặc một cache layer như Redis). Bản ghi này chứa thông tin định danh người dùng (ví dụ:
- (Server → Client) Gửi Cookie: Server gửi
Session ID
về cho client, thường được đặt trong một HTTP cookie (ví dụ:Set-Cookie: sessionId=abc123xyz; HttpOnly
). CờHttpOnly
ngăn JavaScript phía client truy cập vào cookie này, giúp giảm nguy cơ tấn công XSS. - (Client → Server) Các Yêu Cầu Tiếp Theo: Đối với mọi yêu cầu tiếp theo đến cùng một domain, trình duyệt sẽ tự động đính kèm cookie chứa
Session ID
. - (Server) Xác Minh Session: Server nhận
Session ID
từ cookie, tra cứu nó trong kho lưu trữ của mình. Nếu tìm thấy một session hợp lệ, server sẽ biết người dùng là ai và xử lý yêu cầu.
Ví dụ với TypeScript (sử dụng Express và express-session):
// --- Server-side ---
import express from 'express';
import session from 'express-session';
const app = express();
app.use(express.json());
// Cấu hình middleware session
app.use(session({
secret: 'a-very-strong-and-long-secret-key',
resave: false,
saveUninitialized: false, // Không tạo session cho đến khi có dữ liệu được lưu
cookie: {
secure: process.env.NODE_ENV === 'production', // Yêu cầu HTTPS ở môi trường production
httpOnly: true, // Ngăn truy cập từ JavaScript client-side
maxAge: 1000 * 60 * 60 * 24 // 1 ngày
}
}));
// Mở rộng kiểu Request của Express để thêm thuộc tính session
declare module 'express-session' {
interface SessionData {
user?: { id: string; role: string };
}
}
// Route đăng nhập
app.post('/login', (req, res) => {
const { email, password } = req.body;
// ... logic xác thực người dùng từ database ...
const user = { id: 'user-123', role: 'admin' }; // Giả sử xác thực thành công
// Lưu thông tin vào session, express-session sẽ tự xử lý việc tạo và gửi cookie
req.session.user = user;
res.status(200).json({ message: 'Login successful' });
});
// Route được bảo vệ
app.get('/api/profile', (req, res) => {
if (req.session.user) {
res.json({ user: req.session.user });
} else {
res.status(401).json({ error: 'Unauthorized' });
}
});
// Route đăng xuất
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Could not log out.' });
}
res.clearCookie('connect.sid'); // Tên cookie mặc định của express-session
res.status(200).json({ message: 'Logout successful' });
});
});
Ưu điểm:
- Bảo mật thông tin: Các dữ liệu nhạy cảm của người dùng (như vai trò, quyền hạn) đều được lưu trữ trên server, không hiển thị ra phía client nên hạn chế nguy cơ rò rỉ thông tin.
- Kiểm soát hiệu quả: Server có toàn quyền quản lý các phiên đăng nhập. Việc thu hồi một phiên (chẳng hạn khi người dùng đổi mật khẩu hoặc bị khóa tài khoản) rất đơn giản, chỉ cần xóa session tương ứng trên server.
Nhược điểm:
- Tăng tải cho server: Server phải lưu session của tất cả người dùng đang hoạt động, điều này có thể tốn kém về bộ nhớ và tài nguyên khi ứng dụng có nhiều người truy cập.
- Khó mở rộng quy mô: Đây là trở ngại lớn trong môi trường nhiều server (sử dụng load balancer). Các session chỉ được lưu trên một server cụ thể, nên nếu request chuyển sang server khác, dữ liệu session sẽ không còn. Để giải quyết, bạn phải triển khai giải pháp như sử dụng sticky session hoặc lưu session ở hệ thống chia sẻ chung (ví dụ Redis), điều này làm phức tạp thêm cho kiến trúc toàn hệ thống.
2. Xác Thực Bằng JWT (Stateless)
JWT là một tiêu chuẩn mở (RFC 7519) cho phép tạo ra các token chứa thông tin (claims) đã được mã hóa và ký số. Server có thể xác minh tính hợp lệ của token mà không cần lưu trữ bất kỳ trạng thái nào.
Cấu Trúc của một JWT
Một JWT gồm 3 phần, ngăn cách bởi dấu chấm (.
): xxxxx.yyyyy.zzzzz
- Header (Base64Url Encoded): Chứa metadata về token, như thuật toán ký (
alg
) và loại token (typ
).{ "alg": "HS256", "typ": "JWT" }
- Payload (Base64Url Encoded): Chứa các claims (khai báo) về người dùng và các dữ liệu khác. Ví dụ:
sub
(subject/user ID),exp
(expiration time). Lưu ý: Payload chỉ được mã hóa Base64, không được mã hóa, vì vậy không nên đặt thông tin nhạy cảm vào đây.{ "sub": "user-123", "role": "admin", "exp": 1678886400 }
- Signature: Phần quan trọng nhất. Được tạo ra bằng cách kết hợp Header, Payload, một khóa bí mật (secret key chỉ server biết) và áp dụng thuật toán ký đã chỉ định.
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Chữ ký này đảm bảo rằng token không bị sửa đổi trên đường truyền.
Luồng Hoạt Động
- (Client → Server) Đăng nhập: Tương tự như phương pháp session.
- (Server) Xác thực và Tạo Token: Server kiểm tra thông tin. Nếu hợp lệ, server tạo một JWT, ký nó bằng secret key của mình và gửi token về cho client.
- (Client) Lưu trữ Token: Client nhận và lưu trữ JWT (thường trong
localStorage
hoặc bộ nhớ của ứng dụng). - (Client → Server) Các Yêu Cầu Tiếp Theo: Client đính kèm JWT vào header
Authorization
của mỗi yêu cầu cần xác thực, theo chuẩnBearer
.Authorization: Bearer xxxxx.yyyyy.zzzzz
- (Server) Xác Minh Token: Với mỗi yêu cầu, server nhận token từ header, kiểm tra chữ ký bằng secret key. Nếu chữ ký hợp lệ và token chưa hết hạn, server sẽ tin tưởng các thông tin trong payload và xử lý yêu cầu. Server không cần truy vấn database hay cache để tìm thông tin phiên.
Ví dụ với TypeScript (Express & jsonwebtoken
)
// --- Server-side ---
import express from 'express';
import jwt from 'jsonwebtoken';
import { expressjwt, Request as JwtRequest } from 'express-jwt'; // Middleware xác thực
const app = express();
app.use(express.json());
const JWT_SECRET = 'your-super-secret-and-long-key-that-is-not-guessable';
// Route đăng nhập
app.post('/login', (req, res) => {
const { email, password } = req.body;
// ... logic xác thực người dùng từ database ...
const user = { id: 'user-123', role: 'admin' }; // Giả sử xác thực thành công
// Tạo payload và ký token
const payload = { sub: user.id, role: user.role };
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' }); // Access token ngắn hạn
res.json({ accessToken: token });
});
// Middleware để bảo vệ các route. Nó sẽ tự động giải mã token và gắn payload vào req.auth
const authenticate = expressjwt({
secret: JWT_SECRET,
algorithms: ["HS256"],
});
// Route được bảo vệ
// Lưu ý: Request bây giờ là JwtRequest để có `req.auth`
app.get('/api/profile', authenticate, (req: JwtRequest, res) => {
// Middleware `authenticate` đã xác thực token
// Payload có sẵn trong `req.auth`
// req.auth sẽ là { sub: 'user-123', role: 'admin', iat: ..., exp: ... }
res.json({ user: req.auth });
});
Ưu điểm:
- Không lưu trạng thái (Stateless) và dễ mở rộng: Vì server không cần lưu thông tin phiên đăng nhập, việc mở rộng hệ thống chỉ đơn giản là thêm server mới. Miễn là các server cùng chia sẻ secret key, chúng đều có thể xác thực token của người dùng.
- Tách biệt hệ thống: JWT rất phù hợp với những kiến trúc hiện đại như Microservices, ứng dụng SPA (Single Page Application) hoặc ứng dụng di động, nơi frontend và backend hoạt động độc lập với nhau.
Nhược điểm:
- Khó thu hồi token: Sau khi phát hành, JWT sẽ có hiệu lực cho đến khi hết hạn; nếu token bị lộ, kẻ xấu vẫn có thể sử dụng. Việc thu hồi một token trước thời điểm hết hạn khá phức tạp, thường phải dùng đến giải pháp stateful như blocklist, điều này làm mất đi lợi thế stateless vốn có. Có thể tham khảo phương án như Access/Refresh Token
- Kích thước lớn: JWT thường chứa nhiều thông tin hơn so với một session ID thông thường, khiến kích thước request header tăng lên.
- Nguy cơ bảo mật phía client: Nếu JWT được lưu trong localStorage, nó có thể bị đánh cắp qua các lỗ hổng XSS (Cross-Site Scripting).
3. Kết luận
Việc lựa chọn sử dụng Session hay JWT nên dựa vào đặc thù kiến trúc và yêu cầu của từng ứng dụng.
- Nếu bạn đang xây dựng một ứng dụng web monolithic và muốn đơn giản hóa việc quản lý session cũng như dễ dàng kiểm soát, vô hiệu hóa tài khoản bất cứ lúc nào, Session sẽ là lựa chọn phù hợp hơn.
- Ngược lại, nếu dự án của bạn hướng đến các kiến trúc hiện đại như hệ thống phân tán, Microservices, SPA hay ứng dụng di động, JWT sẽ phát huy tối đa lợi thế nhờ khả năng mở rộng linh hoạt và hỗ trợ tách biệt các thành phần trong hệ thống.