Giải Thích JWT Tokens: Cấu Trúc, Bảo Mật và Những Lỗi Thường Gặp
Tìm hiểu cách JSON Web Tokens hoạt động, nội dung bên trong chúng, và những cạm bẫy bảo mật khiến các lập trình viên gặp rắc rối khi triển khai thực tế.
JSON Web Tokens (JWTs) có mặt ở khắp nơi — chúng hỗ trợ xác thực trong các SPA, ứng dụng di động và kiến trúc microservice trên toàn thế giới. Thế nhưng đây cũng là một trong những cơ chế bảo mật bị hiểu nhầm nhiều nhất trong phát triển web. Dùng sai có thể dẫn đến bỏ qua xác thực, leo thang đặc quyền, hoặc chiếm đoạt toàn bộ tài khoản.
JWT là gì?
JWT là một chuỗi nhỏ gọn, an toàn với URL, mã hóa một tập hợp các claims — những khẳng định về người dùng hoặc phiên làm việc. Nó trông như thế này:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Đây là ba đoạn được mã hóa Base64URL, ngăn cách nhau bằng dấu chấm:
- Header — thuật toán và loại token
- Payload — các claims (dữ liệu người dùng)
- Signature — bằng chứng mật mã về tính toàn vẹn
Dán bất kỳ JWT nào vào JWT Decoder của chúng tôi để kiểm tra ngay cả ba phần mà không cần liên hệ với bất kỳ máy chủ bên ngoài nào.
Cấu trúc của một JWT
Header
{
"alg": "HS256",
"typ": "JWT"
}
alg chỉ định thuật toán ký. Các giá trị phổ biến:
HS256— HMAC với SHA-256 (đối xứng, dùng chung một secret)RS256— RSA với SHA-256 (bất đối xứng, khóa công khai/riêng tư)ES256— ECDSA với SHA-256 (bất đối xứng, khóa nhỏ hơn)
Payload
{
"sub": "user_abc123",
"email": "alice@example.com",
"role": "admin",
"iat": 1711670400,
"exp": 1711756800
}
Các claims chuẩn đã được đăng ký:
| Claim | Ý nghĩa |
|---|---|
sub |
Subject (token dành cho ai) |
iss |
Issuer (ai đã tạo token) |
aud |
Audience (ai được phép chấp nhận token) |
exp |
Thời gian hết hạn (Unix timestamp) |
iat |
Thời điểm phát hành (Unix timestamp) |
nbf |
Not before (không chấp nhận trước thời điểm này) |
Signature
Với HS256:
HMAC-SHA256(base64url(header) + "." + base64url(payload), secret)
Signature chứng minh token chưa bị giả mạo. Payload KHÔNG được mã hóa — nó chỉ được encode. Bất kỳ ai cũng có thể đọc được.
Tuyệt đối không lưu dữ liệu nhạy cảm như mật khẩu hay số thẻ tín dụng trong JWT payload.
HS256 vs. RS256: nên chọn cái nào?
HS256 sử dụng một secret dùng chung. Mọi dịch vụ cần xác minh token đều phải có cùng secret đó. Đơn giản cho các ứng dụng monolith, nhưng nguy hiểm trong kiến trúc đa dịch vụ — nếu bất kỳ dịch vụ nào bị xâm phạm, kẻ tấn công có thể giả mạo token.
RS256 sử dụng cặp khóa bất đối xứng. Máy chủ xác thực ký bằng khóa riêng tư; tất cả các dịch vụ khác xác minh bằng khóa công khai. Việc một dịch vụ tiêu thụ bị xâm phạm không cho phép kẻ tấn công giả mạo token. Ưu tiên dùng RS256 cho bất kỳ hệ thống nào có nhiều dịch vụ.
Lỗ hổng alg: none
Một trong những cuộc tấn công JWT khét tiếng nhất. Một số thư viện cũ chấp nhận token với "alg": "none" và không có signature — coi nó là hợp lệ. Kẻ tấn công có thể tạo ra:
{ "alg": "none" }
với bất kỳ payload nào và vượt qua xác thực hoàn toàn.
Cách khắc phục: Luôn chỉ định rõ ràng các thuật toán được phép trong thư viện JWT của bạn. Không bao giờ chấp nhận none.
// ❌ Nguy hiểm
jwt.verify(token, secret);
// ✅ An toàn — chỉ chấp nhận HS256
jwt.verify(token, secret, { algorithms: ["HS256"] });
Tấn công nhầm lẫn thuật toán
Một lỗ hổng nghiêm trọng khác: nếu thư viện của bạn tự động phát hiện thuật toán từ header, kẻ tấn công có thể đổi RS256 thành HS256 và ký token bằng khóa công khai làm HMAC secret (vốn dĩ là công khai theo định nghĩa).
Cách khắc phục: Luôn hardcode thuật toán dự kiến. Không bao giờ tin tưởng header alg.
Lưu trữ token: nên để JWT ở đâu
| Nơi lưu trữ | Rủi ro XSS | Rủi ro CSRF | Ghi chú |
|---|---|---|---|
localStorage |
Cao | Không có | Mọi JS trên trang đều truy cập được |
sessionStorage |
Cao | Không có | Bị xóa khi đóng tab |
| HTTP-only cookie | Không có | Trung bình | Tốt nhất cho web app; dùng SameSite=Strict |
| Memory (biến) | Thấp | Không có | Mất khi làm mới trang; dành cho SPA |
Với ứng dụng web, HTTP-only cookies với SameSite=Strict là lựa chọn an toàn nhất. Với ứng dụng native, nên dùng các API lưu trữ bảo mật (Keychain, Keystore).
Hết hạn và refresh tokens
Access token có thời hạn ngắn (5–15 phút) kết hợp với refresh token có thời hạn dài là mô hình chuẩn:
- Người dùng đăng nhập → máy chủ cấp access token (15 phút) + refresh token (7 ngày, lưu trong DB)
- Client dùng access token cho các API call
- Khi access token hết hạn, client gửi refresh token → nhận access token mới
- Khi đăng xuất, vô hiệu hóa refresh token trong database
Cách này giới hạn thiệt hại nếu access token bị đánh cắp.
Thu hồi JWTs
JWT là stateless — một khi đã phát hành, chúng hợp lệ cho đến khi hết hạn. Đây là một sự đánh đổi. Các lựa chọn để thu hồi sớm:
- Blocklist — lưu các giá trị JTI (JWT ID) đã bị vô hiệu hóa trong Redis. Kiểm tra mỗi request.
- Thời hạn ngắn — token 5 phút giảm thiểu phạm vi thiệt hại.
- Refresh token rotation — phát hiện việc tái sử dụng token đã được xoay vòng như một dấu hiệu bị đánh cắp.
Gỡ lỗi với JWT Decoder
Khi gỡ lỗi các vấn đề xác thực, hãy dùng JWT Decoder của chúng tôi để:
- Kiểm tra toàn bộ payload mà không cần viết code
- Xem timestamps hết hạn ở dạng dễ đọc
- Xác minh thuật toán đang được sử dụng
- Phát hiện các claims không mong muốn hoặc còn thiếu
Tất cả quá trình giải mã diễn ra cục bộ trên trình duyệt của bạn — token không bao giờ rời khỏi máy của bạn.
Checklist bảo mật JWT
- Dùng RS256 hoặc ES256 cho kiến trúc đa dịch vụ
- Chỉ định rõ ràng các thuật toán được phép trong thư viện của bạn
- Xác thực các claims
exp,issvàaudtrên mỗi request - Lưu token trong HTTP-only cookies cho web app
- Sử dụng thời hạn access token ngắn (15 phút hoặc ít hơn)
- Triển khai refresh token rotation với tính năng phát hiện tái sử dụng
- Không bao giờ đưa dữ liệu nhạy cảm vào payload
- Dùng HTTPS ở mọi nơi — token dạng plaintext cũng như không có xác thực
JWT mạnh mẽ và tiện lợi, nhưng mức độ bảo mật phụ thuộc hoàn toàn vào cách triển khai. Hiểu rõ cấu trúc, nắm vững các kiểu tấn công, và lớp xác thực của bạn sẽ vững chắc.