Zoom-in: JWT

Đăng nhập, gọi API, server cho qua. Đây là cơ chế xác thực mà AI generate ra hàng ngàn lần mỗi ngày mà không giải thích tại sao nó hoạt động.
graph LR
C(["👤 Client"]) -->|"token"| S(["🖥️ Server"])
S -->|"200 OK"| C
style C fill:#1e293b,stroke:#475569,color:#cbd5e1
style S fill:#1e293b,stroke:#475569,color:#cbd5e1
Phóng to dần vào black box đó.
Layer 1 — Vấn đề gốc: server không nhớ
HTTP là stateless — mỗi request độc lập, server không nhớ request trước. Người dùng đã đăng nhập, nhưng request tiếp theo server không biết ai đang hỏi.
sequenceDiagram
participant C as Client
participant S as Server
C->>S: POST /login (email + password)
S-->>C: OK, nhưng request sau server quên luôn
C->>S: GET /dashboard
S-->>C: Ai vậy? 401 Unauthorized
Cần cơ chế để client "chứng minh danh tính" trong mỗi request — mà không phải gửi lại password mỗi lần.
Layer 2 — Session truyền thống: server ghi nhớ thay client
Giải pháp cổ điển: sau khi đăng nhập, server tạo một session ID ngẫu nhiên, lưu vào database, rồi gửi lại cho client qua cookie.
sequenceDiagram
participant C as Client
participant S as Server
participant DB as Database
C->>S: POST /login
S->>DB: Lưu session {id: "abc123", user_id: 42}
S-->>C: Set-Cookie: session=abc123
C->>S: GET /dashboard (Cookie: session=abc123)
S->>DB: Tìm session "abc123"
DB-->>S: {user_id: 42}
S-->>C: 200 OK — dữ liệu của user 42
Hoạt động tốt — nhưng mỗi request đều phải query database để xác nhận session. Với nhiều server (horizontal scaling), mọi server cần truy cập cùng session store.
Layer 3 — JWT: token tự chứng minh
JWT đảo ngược cách tiếp cận: thay vì server lưu trạng thái, token tự mang thông tin và chứng minh tính hợp lệ của chính nó.
Một JWT gồm 3 phần, ngăn cách bởi dấu .:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0MiwiZXhwIjoxNzUwMDAwMDAwfQ.xK9mF2...
HEADER PAYLOAD SIGNATURE
HS256, RS256...) — Base64 encode, không mã hóa
user_id, role, exp (thời hạn)... — Base64 encode, không mã hóa
HMAC(header + payload, secret_key) — bảo đảm token không bị giả mạo
Bất kỳ ai cũng có thể Base64-decode header và payload để đọc nội dung. Điều bảo vệ token không phải là mã hóa — mà là chữ ký.
Layer 4 — Chữ ký số: tại sao không thể làm giả
Server ký token bằng secret key — một chuỗi ngẫu nhiên chỉ server biết:
signature = HMAC-SHA256(base64(header) + "." + base64(payload), SECRET_KEY)
Khi nhận token, server tính lại chữ ký từ header và payload, rồi so sánh với chữ ký trong token:
graph TD
Req["GET /dashboard\nAuthorization: Bearer eyJ..."] --> Split["Tách token\nheader | payload | signature"]
Split --> Recalc["Tính lại:\nHMAC(header + payload, SECRET_KEY)"]
Recalc --> Compare{"So sánh\nchữ ký"}
Compare -->|"Khớp + chưa hết hạn"| OK["200 OK — user_id=42"]
Compare -->|"Không khớp / hết hạn"| Fail["401 Unauthorized"]
style Req fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style Split fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style Recalc fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style Compare fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style OK fill:#1a3a2a,stroke:#22c55e,color:#86efac
style Fail fill:#3b1a1a,stroke:#ef4444,color:#fca5a5
Nếu ai đó thay đổi payload (ví dụ đổi user_id từ 42 thành 1) thì chữ ký sẽ không khớp nữa — vì họ không có SECRET_KEY để ký lại. Server từ chối ngay lập tức.
Full picture
Từ cookie/session đến JWT stateless.
graph TD
Login["POST /login\nemail + password"] --> Verify["Xác thực\ncredentials"]
Verify --> Sign["Ký JWT\nHMAC(header+payload, SECRET)"]
Sign --> Client["Client\nlưu token"]
Client --> Req["GET /dashboard\nBearer eyJ..."]
Req --> Check["Server xác thực\ntính lại chữ ký"]
Check -->|"Khớp + chưa hết hạn"| Allow["200 OK"]
Check -->|"Không khớp / hết hạn"| Deny["401 Unauthorized"]
style Login fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style Client fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style Req fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style Verify fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style Sign fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style Check fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style Allow fill:#1a3a2a,stroke:#22c55e,color:#86efac
style Deny fill:#3b1a1a,stroke:#ef4444,color:#fca5a5
Ba nhầm lẫn phổ biến
JWT được mã hóa
Header và payload chỉ được Base64 encode — không phải mã hóa. Bất kỳ ai có token đều đọc được nội dung. Không lưu thông tin nhạy cảm (password, số thẻ...) trong payload. Chữ ký chỉ đảm bảo không bị thay đổi, không đảm bảo bí mật.
Lưu JWT trong localStorage là ổn
localStorage dễ bị tấn công XSS — bất kỳ script nào trên trang đều đọc được. httpOnly cookie không thể truy cập từ JavaScript, là lựa chọn an toàn hơn cho token dài hạn. Nhiều AI-generated auth code mặc định dùng localStorage mà không giải thích trade-off này.
JWT không thể thu hồi
Đúng theo thiết kế — server không lưu trạng thái nên không có danh sách token hợp lệ để xóa. Giải pháp thực tế: dùng exp ngắn (15 phút) kết hợp refresh token. Hoặc duy trì blocklist token đã thu hồi — lúc này lại cần DB, mất một phần lợi thế stateless.
Takeaway
JWT tồn tại để giải quyết session store tập trung — không phải để bảo mật hơn session cookie. Chữ ký HMAC đảm bảo token không bị giả mạo; Base64 không ẩn dữ liệu. Stateless có giá: token không thu hồi được, nên thời hạn ngắn và refresh token là bắt buộc.
Khi debug auth, câu hỏi đúng: "token hết hạn, bị giả mạo, hay bị từ chối đúng?" — exp, chữ ký, và status code 401 vs 403 sẽ chỉ ra tầng nào có vấn đề.
Bài viết được hỗ trợ bởi Amy 🌸 - AI Assistant. Nội dung đã được kiểm duyệt bởi tác giả.
Related Posts
Zoom-in: OAuth 2.0
'Đăng nhập bằng Google' ẩn sau đó một cơ chế ủy quyền mà không bao giờ để password rời khỏi Google. OAuth 2.0 giải quyết bài toán delegation mà không hi sinh bảo mật.
Zoom-in: Asymmetric Encryption
HTTPS an toàn vì được mã hóa. Nhưng ai đã mã hóa, ai giải mã, và tại sao không ai giả mạo được server — đó mới là câu hỏi thật sự.
Zoom-in: TCP
Mọi HTTP request đều đi trên TCP — nhưng trước khi byte đầu tiên của dữ liệu đi qua, đã có 3 gói tin trao đổi mà không mang dữ liệu nào. TCP giải quyết vấn đề mà Internet không giải quyết được.