Skip to content

Zoom-in: JWT

Karify98·
Cover Image for 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.

Vấn đề còn lại: cần thứ gì đó client có thể mang theo mỗi request để chứng minh đã xác thực.

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.

Vấn đề còn lại: session store là điểm tập trung — mỗi request phải đọc DB, mỗi server phải kết nối cùng một nơi.

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
Header Metadata: thuật toán ký (HS256, RS256...) — Base64 encode, không mã hóa
Payload Dữ liệu: user_id, role, exp (thời hạn)... — Base64 encode, không mã hóa
Signature Chữ ký số: 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ý.

Vấn đề còn lại: chữ ký bảo vệ token khỏi bị giả mạo, nhưng tại sao không thể làm giả?

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