blog.dopana

Back

JSON Web Token (JWT) là một trong những chuẩn xác thực phổ biến nhất trong thế giới phát triển web hiện đại. Từ các ứng dụng Single Page Application (React, Vue, Svelte) cho đến hệ thống Microservices hay Mobile Apps, JWT xuất hiện ở hầu khắp mọi kiến trúc xác thực.

Tuy nhiên, hiểu và sử dụng JWT đúng cách và an toàn lại là bài toán khiến không ít lập trình viên đau đầu: Nên lưu JWT ở đâu? Xử lý hết hạn phiên ra sao? Làm thế nào để chống cả XSS lẫn CSRF?

Hãy cùng tìm hiểu toàn diện kiến trúc Web Authentication với JWT từ góc nhìn trực quan nhất.

1. Giải thích theo kiểu ELI5: Tủ gửi đồ vs. Vòng tay VIP#

Để hiểu tại sao JWT ra đời, hãy so sánh nó với phương pháp Session truyền thống qua ví dụ tại một công viên giải trí:

Cách truyền thống (Session-based): “Tủ gửi đồ và thẻ số”#

  • Khi vào cổng, lễ tân cấp cho bạn một chiếc thẻ số (Session ID).
  • Mọi thông tin của bạn (Tên, gói vé VIP, giờ vào) được ghi vào một cuốn sổ cái tổng ở quầy tiếp tân (Database hoặc Redis).
  • Mỗi khi bạn muốn chơi tàu lượn, nhân viên soát vé phải cầm bộ đàm gọi về quầy tiếp tân: “Kiểm tra giúp thẻ số 102 này có mua vé VIP không?”.
  • Vấn đề: Khi công viên có 100.000 khách và 50 trò chơi, tổng đài quầy tiếp tân sẽ bị quá tải vì hàng triệu lượt gọi tra cứu sổ cái liên tục.

Cách hiện đại (JWT-based): “Vòng tay điện tử đóng dấu số”#

  • Khi vào cổng, ban quản lý trao cho bạn một chiếc vòng đeo tay thông minh (JWT).
  • Trên vòng tay in sẵn thông tin: Tên: Alice, Hạng: VIP, Hết hạn: 18h00.
  • Đặc biệt, ban quản lý dán lên vòng tay một con dấu mật mã chống giả (Chữ ký điện tử - Digital Signature).
  • Khi bạn chơi bất kỳ trò nào, nhân viên chỉ cần nhìn con dấu mật mã để xác nhận vòng tay là thật và đọc thẳng thông tin VIP trên vòng tay mà không cần gọi điện hỏi ai.
flowchart TD
    subgraph Traditional["1. Session truyền thống - Stateful"]
        Client1["Client"] -->|Gửi Session ID| Server1["App Server"]
        Server1 -->|Tra cứu DB mỗi request| DB["Database / Redis<br/>Session Store"]
    end

    subgraph JWTWay["2. Xác thực JWT - Stateless"]
        Client2["Client"] -->|Gửi JWT Token| Server2["App Server"]
        Server2 -->|Tự xác thực chữ ký tại chỗ| Server2
    end

2. Cấu trúc 3 phần của một JSON Web Token#

Một chuỗi JWT gồm 3 phần được mã hóa Base64URL và nối với nhau bằng dấu chấm (.):

Header.Payload.Signature\text{Header} . \text{Payload} . \text{Signature}

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiQWxpY2UiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3MDAwMDAwMDB9.4a5b6c...
text
  1. Header (Đầu trang): Chứa loại token (JWT) và thuật toán ký mã hóa (ví dụ HS256 hoặc RS256).
  2. Payload (Dữ liệu tải): Chứa các thông tin trao đổi (Claims) như sub (User ID), role (Quyền hạn), exp (Thời gian hết hạn), iat (Thời điểm tạo).
  3. Signature (Chữ ký mật mã): Được tạo ra bằng cách lấy Header + Payload băm với một Secret Key bí mật nằm trên server.

[!WARNING] Payload KHÔNG hề được mã hóa bí mật! Nó chỉ được encode Base64Url để truyền tải trên mạng. Bất kỳ ai cũng có thể giải mã và đọc được nội dung Payload. Tuyệt đối không lưu mật khẩu, thông tin thẻ tín dụng hay dữ liệu nhạy cảm vào Payload của JWT!

3. Kiến trúc chuẩn: Mô hình Dual-Token (Access & Refresh Token)#

Nếu chỉ sử dụng một Access Token duy nhất:

  • Nếu đặt thời hạn quá dài (ví dụ 30 ngày) \rightarrow Rất nguy hiểm nếu token bị rò rỉ.
  • Nếu đặt thời hạn quá ngắn (ví dụ 15 phút) \rightarrow Người dùng bị văng ra bắt đăng nhập lại liên tục.

Giải pháp tối ưu cho ứng dụng web là Mô hình 2 Token (Dual-Token Flow):

  • Access Token: Có thời hạn rất ngắn (10 - 15 phút), dùng để gửi kèm vào mỗi API request.
  • Refresh Token: Có thời hạn dài hơn (7 - 30 ngày), chỉ dùng khi Access Token hết hạn để xin cấp mới mà không làm phiền người dùng.
flowchart TD
    User["Client Browser"]
    AuthServer["Auth API Server"]
    ResourceServer["Protected Resource Server"]

    User -->|1. POST /login - Gửi thông tin đăng nhập| AuthServer
    AuthServer -->|2. Trả về Access Token và Refresh Token| User
    
    User -->|3. Gọi API với Bearer Access Token| ResourceServer
    ResourceServer -->|4. Phản hồi dữ liệu 200 OK| User

    User -->|5. Token hết hạn 401 Unauthorized| ResourceServer
    User -->|6. POST /refresh với Cookie HttpOnly| AuthServer
    AuthServer -->|7. Cấp Access Token mới - Token Rotation| User

4. Nên lưu trữ JWT ở đâu trên Web Client? (XSS vs. CSRF)#

Đây là chủ đề gây tranh luận nhiều nhất trong giới bảo mật web:

Vị trí lưu trữNguy cơ XSSNguy cơ CSRFĐánh giá
localStorage / sessionStorage❌ Rất cao (Bất kỳ mã JS độc hại nào cũng đọc và trộm được token)✅ An toàn (Trình duyệt không tự động gửi)Không khuyến nghị cho Token dài hạn
Cookie thường (Không HttpOnly)❌ Rất cao (Đọc được qua document.cookie)❌ Cao (Trình duyệt tự đính kèm)Rất kém an toàn
HttpOnly + Secure + SameSite=Strict/Lax Cookie✅ An toàn (JavaScript không thể đọc trộm)✅ An toàn (SameSite chặn request cross-site)Khuyến nghị chuẩn cho Refresh Token
Bộ nhớ RAM (In-Memory JS Variable)✅ An toàn (Biến mất khi đóng tab, khó trộm hàng loạt)✅ An toàn (Không tự động gửi)Khuyến nghị chuẩn cho Access Token

Chiến lược lưu trữ Best Practice#

  1. Access Token: Lưu tạm trong Memory (State của React/Vue/Svelte).
  2. Refresh Token: Lưu trong HttpOnly Cookie đi kèm cờ SecureSameSite=Lax (hoặc Strict).
  3. Khi người dùng F5 hoặc mở tab mới, client tự động gọi /auth/refresh một lần (Silent Refresh) để lấy Access Token nạp vào RAM.

5. Cài đặt thực tế với Node.js / Express & TypeScript#

5.1. Tạo Token khi Đăng nhập#

5.2. Middleware Xác thực Access Token#

5.3. Endpoint Cấp Mới Token (Refresh Token Endpoint)#

6. Những sai lầm bảo mật kinh điển cần tránh#

[!TIP] Checklist an toàn tuyệt đối cho JWT:

  1. Khóa bí mật mạnh: Luôn dùng secret key dài tối thiểu 256-bit hoặc sử dụng cặp khóa bất đối xứng RSA (RS256) / Elliptic Curve (ES256).
  2. Chặn thuật toán "alg": "none": Các thư viện JWT cũ từng dính lỗ hổng chấp nhận token không cần ký nếu header ghi "alg": "none".
  3. Kiểm tra Audience (aud) và Issuer (iss): Xác minh token được phát hành bởi đúng máy chủ của bạn và dành cho đúng đối tượng client.
  4. Triển khai Token Rotation: Mỗi khi Refresh Token được dùng để cấp Access Token mới, hãy hủy Refresh Token cũ và cấp một Refresh Token hoàn toàn mới.

7. Tổng kết#

Sử dụng JWT cho Web Authentication mang lại khả năng mở rộng (scalability) vượt trội cho các ứng dụng phân tán. Bằng cách áp dụng mô hình Dual-Token, lưu trữ Access Token trong Memory và Refresh Token trong HttpOnly Cookie, bạn sẽ xây dựng được một hệ thống xác thực vừa tiện lợi, mượt mà vừa đạt tiêu chuẩn bảo mật doanh nghiệp.

Tài liệu tham khảo#