JWT (JSON Web Token)

JWT (JSON Web Token) là một tiêu chuẩn mở (RFC 7519) dùng để chia sẻ thông tin an toàn giữa các bên (ví dụ: giữa giao diện Web/App và máy chủ Backend) dưới dạng một chuỗi ký tự JSON nhỏ gọn.

JWT thường được áp dụng nhiều nhất cho hai bài toán:

  1. Xác thực người dùng (Authentication): Đăng nhập một lần, máy chủ cấp token để người dùng dùng token đó gọi các API tiếp theo.

  2. Trao đổi thông tin (Information Exchange): Cho phép các dịch vụ kiểm tra tính toàn vẹn của dữ liệu nhờ cơ chế ký số.

1. Cấu trúc của một chuỗi JWT

Một chuỗi JWT gồm 3 phần tách biệt nhau bởi dấu chấm (.): Header.Payload.Signature

A. Header (Phần đầu)

Chứa thông tin về loại token (thường là JWT) và thuật toán mã hóa băm dùng để tạo chữ ký (ví dụ: HS256 hoặc RS256).

{
  "alg": "HS256",
  "typ": "JWT"
}

Phần này sau đó được mã hóa dạng Base64URL.

B. Payload (Nội dung dữ liệu)

Chứa dữ liệu thực sự muốn truyền đi (gọi là các Claims), ví dụ: ID người dùng, quyền hạn, thời gian hết hạn (exp).

JSON
{
  "user_id": 1024,
  "role": "admin",
  "exp": 1700000000
}

Phần này cũng được mã hóa dạng Base64URL.

⚠️ LƯU Ý QUAN TRỌNG: Base64 không phải là mã hóa bảo mật (Encryption) mà chỉ là một cách biến đổi định dạng chuỗi. Bất kỳ ai lấy được chuỗi JWT đều có thể dịch ngược (Decode) ra để đọc thông tin trong Payload. Do đó, tuyệt đối không lưu mật khẩu hay thông tin nhạy cảm vào Payload.

C. Signature (Chữ ký điện tử)

Phần giúp chống giả mạo dữ liệu. Chữ ký được tạo bằng cách lấy:

Base64(Header) + . + Base64(Payload) đem đi băm qua thuật toán (ví dụ HMAC-SHA256) cùng với một Secret Key bí mật nằm ở Server.

$$\text{Signature} = \text{HMAC-SHA256}(\text{Base64}(Header) + "." + \text{Base64}(Payload), \text{SecretKey})$$

2. JWT hoạt động như thế nào trong luồng Đăng nhập?

Plaintext
[ Client (App/Web) ]                      [ Server (Django/NodeJS) ]
        │                                             │
        │ 1. POST /login (username, password)        │
        ├────────────────────────────────────────────►│
        │                                             │ 2. Xác thực MK đúng.
        │                                             │    Tạo JWT với Secret Key.
        │ 3. Trả về Token: { access_token: "..." }    │
        │◄────────────────────────────────────────────┤
        │                                             │
        │ 4. POST /api/courses                        │
        │    Header: Authorization: Bearer <Token>    │
        ├────────────────────────────────────────────►│
        │                                             │ 5. Server lấy Token, tính lại
        │                                             │    Signature bằng Secret Key.
        │                                             │    -> Nếu khớp: Trả dữ liệu!
        │ 6. Trả về dữ liệu Khóa học                  │    -> Nếu sai: Báo lỗi 401!
        │◄────────────────────────────────────────────┘

3. Tại sao JWT lại được ưu chuộng thay cho Session truyền thống?

Tiêu chí Session / Cookie truyền thống JWT (JSON Web Token)
Nơi lưu trữ Server phải lưu Session ID vào Memory/Redis/Database. Server không lưu trữ gì (Stateless). Token nằm hoàn toàn ở Client.
Khả năng mở rộng (Scalability) Khó mở rộng khi có nhiều Server (cần dùng Session DB chung như Redis). Rất dễ. Bất kỳ Server nào nắm giữ Secret Key đều có thể giải mã và xác thực token.
Cross-Domain / Mobile Cấu hình Cookie trên Mobile App hoặc nhiều Domain khác nhau rất phức tạp. Dễ dàng truyền qua HTTP Header (Authorization: Bearer ...), chạy tốt trên cả Web và Mobile (Flutter, React Native).

Dưới đây là giải thích chi tiết cho sơ đồ trên :

1. Dữ liệu được ký là gì? Có phải là username và password không?

  • KHÔNG BAO GỒM PASSWORD! Lỗi bảo mật rất nguy hiểm nếu đưa password vào JWT.

  • Quy trình chuẩn:

    1. Ở bước 1, Browser gửi username và password lên để Server kiểm tra với Database.

    2. Khi xác thực đúng, Server sẽ vứt bỏ password.

    3. Server tạo ra cấu trúc JWT gồm 3 phần (cách nhau bởi dấu chấm .): Header.Payload.Signature

      • Header: Chứa thuật toán mã hóa (ví dụ: {"alg": "HS256", "typ": "JWT"}).

      • Payload: Chứa thông tin người dùng công khai (ví dụ: {"user_id": 123, "role": "admin", "exp": 1700000000}).

      • Signature: Được tạo ra bằng cách lấy (Header + Payload) đem đi ký với Key.

2. Sự khác biệt giữa Secret Key trong sơ đồ và Public/Private Key (Chữ ký số)

Sở dĩ anh không thấy sự xuất hiện của Public Key trong sơ đồ này là do sơ đồ đang sử dụng Mã hóa đối xứng (Symmetric Encryption).

Trong JWT có 2 cơ chế tạo chữ ký phổ biến:

Cơ chế 1: Thuật toán HMAC (Ví dụ: HS256) — Như trong sơ đồ của anh

  • Cách hoạt động: Dùng duy nhất 1 chìa khóa bí mật (Secret Key) cho cả 2 việc: Tạo ra Signature và Kiểm tra Signature.

  • Tại sao không có Public Key? Vì chỉ có duy nhất một Server (hoặc các Server chung backend) tự tạo token và tự xác thực token đó.

  • Công thức: Signature = HMAC-SHA256(Header + "." + Payload, Secret_Key)

Cơ chế 2: Thuật toán RSA/ECDSA (Ví dụ: RS256) — Đây mới chính là "Chữ ký số" truyền thống

  • Cách hoạt động: Dùng cặp khóa Bất đối xứng (Asymmetric):

    • Private Key (Khóa riêng): Giữ kín ở Auth Server dùng để ký (Sign) tạo Signature.

    • Public Key (Khóa công khai): Công khai cho các Service khác dùng để xác thực (Verify) Signature.

  • Khi nào dùng? Trong hệ thống Microservices hoặc Single Sign-On (SSO).

    • Ví dụ: Server Đăng nhập (Auth Server) giữ Private Key để cấp Token. Các Microservice khác (Order Service, Payment Service...) chỉ cần giữ Public Key để kiểm tra Token có hợp lệ hay không mà không cần lo lắng bị lộ Private Key.

3. Signature có phải là Token gửi về Browser không?

  • Signature KHÔNG PHẢI là toàn bộ Token, nó chỉ là phần thứ 3 của Token.

  • Chuỗi Token hoàn chỉnh gửi về Browser lưu trữ (ở LocalStorage / Cookie) có dạng:

    $$\text{Token} = \text{Base64(Header)} \ .\ \text{Base64(Payload)} \ .\ \text{Signature}$$
  • Cách Server kiểm tra Token ở request tiếp theo (Bước 5 trong sơ đồ):

    1. Browser gửi chuỗi Token lên qua Header Authorization: Bearer <Token>.

    2. Server tách chuỗi Token thành 3 phần: Header, Payload, và Signature_gửi_lên.

    3. Server lấy Header + Payload vừa nhận, kết hợp với Secret Key sẵn có trên Server để tự tính lại một chữ ký mới (Signature_tính_lại).

    4. Server so sánh:

      • Nếu Signature_tính_lại == Signature_gửi_lên $\rightarrow$ Hợp lệ! (Chứng tỏ Payload chưa bị ai chỉnh sửa trên đường truyền).

      • Nếu không khớp $\rightarrow$ Báo lỗi 401 Unauthorized! (Token đã bị can thiệp).

Tóm tắt so sánh giữa 2 loại Key trong JWT

Tiêu chí Secret Key (Symmetric - HS256) PNG Public / Private Key (Asymmetric - RS256)
Số lượng Key

1 Key duy nhất (Dùng chung cho Sign & Verify).

2 Keys (Private Key để Sign, Public Key để Verify).
Sự xuất hiện của Public Key

Không có (Như sơ đồ của anh).

Có (Public Key công khai cho bên xác thực).
Mô hình phù hợp Monolith (Web app đơn giản 1 backend). Microservices, Hệ thống SSO, OAuth2 (Google/Facebook Login).
Hiệu năng Nhanh hơn, tốn ít tài nguyên CPU. Chậm hơn một chút do thuật toán bất đối xứng phức tạp hơn

ếu hacker can thiệp và lấy được Token ở Bước 3, chúng hoàn toàn có thể dùng Token đó để mạo danh bạn gửi request lên Server, và Server vẫn trả lời request đó bình thường.

Hiện tượng này trong bảo mật gọi là Token Theft (Đánh cắp Token) hay Bearer Token Misuse.

Tại sao Server lại "mắc bẫy" và vẫn trả lời Hacker?

Do bản chất của JWT (JSON Web Token) là Stateless (Server không lưu trạng thái phiên đăng nhập) và hoạt động theo cơ chế Bearer Token ("ai cầm token thì người đó là chủ"):

  1. Server không phân biệt được người gửi: Server không hề biết request đến từ trình duyệt của bạn hay từ công cụ của hacker.

  2. Signature vẫn hợp lệ: Token mà hacker dùng chính là Token thật do Server phát ra ở Bước 3 (chưa bị chỉnh sửa nội dung), nên khi Server tính toán lại Signature thì kết quả vẫn trùng khớp hoàn toàn.

  3. Thời hạn vẫn còn: Nếu Token đó chưa hết hạn (exp), Server sẽ coi đó là một request hợp lệ và trả về dữ liệu.

Hacker có thể lấy được Token bằng những cách nào?

  • Nghe lén trên đường truyền (Man-in-the-Middle): Nếu kết nối giữa Client và Server không sử dụng mã hóa HTTPS (mà dùng HTTP thường), hacker ở cùng mạng WiFi công cộng có thể bắt được gói tin chứa Token ở Bước 3 hoặc Bước 4.

  • Tấn công XSS (Cross-Site Scripting): Nếu Token được lưu ở localStorage hoặc sessionStorage của trình duyệt, hacker có thể chèn mã độc JavaScript vào trang web để lấy cắp chuỗi Token này và gửi về server của hacker.

Làm thế nào để ngăn chặn hoặc giảm thiểu rủi ro này?

Trong thực tế, các lập trình viên áp dụng kết hợp nhiều biện pháp bảo mật sau để bảo vệ JWT:

1. Bắt buộc dùng HTTPS (SSL/TLS)

Mã hóa toàn bộ đường truyền giữa Client và Server. Cho dù hacker có bắt được gói tin trên đường truyền ở Bước 3 hay 4 thì cũng chỉ thấy các chuỗi dữ liệu đã được mã hóa, không thể đọc hay lấy được Token.

2. Lưu trữ Token an toàn với HttpOnly Cookie

  • Thay vì trả Token về dạng JSON rồi lưu vào localStorage, Server sẽ trả Token về qua Cookie và gắn cờ HttpOnly, Secure, SameSite.

  • Cờ HttpOnly ngăn không cho mã JavaScript (kể cả mã độc XSS) truy cập vào Cookie. Trình duyệt sẽ tự động đính kèm Cookie này trong mỗi request gửi lên Server.

3. Cấu hình thời gian sống rất ngắn cho Access Token (Short-lived Access Token)

  • Đặt thời gian hết hạn (exp) của Access Token cực ngắn (ví dụ: 5 đến 15 phút).

  • Kết hợp sử dụng Refresh Token (có thời hạn dài hơn, ví dụ: 7 ngày, lưu ở nơi cực kỳ an toàn).

  • Hiệu quả: Nếu hacker có lỡ lấy cắp được Access Token, chúng chỉ có thể lợi dụng nó trong vài phút ngắn ngủi trước khi Token đó trở nên vô giá trị.

4. Cơ chế Refresh Token Rotation & Thu hồi Token (Blacklist)

  • Mỗi khi Refresh Token được sử dụng để lấy Access Token mới, Server sẽ hủy Refresh Token cũ và cấp một cái hoàn toàn mới.

  • Nếu phát hiện 1 Refresh Token cũ được dùng lại (dấu hiệu bị hacker đánh cắp), Server sẽ ngay lập tức hủy toàn bộ chuỗi Token của tài khoản đó, bắt buộc người dùng (và cả hacker) phải đăng nhập lại từ đầu.

 

4. Cơ chế Cặp Token: Access Token & Refresh Token

Để đảm bảo an toàn, hệ thống thực tế thường áp dụng bộ đôi token:

  1. Access Token:

    • Dùng để gọi các API lấy dữ liệu.

    • Có thời hạn sống rất ngắn (ví dụ: 15 phút - 1 giờ) để nếu lỡ bị lộ thì tác hại bị hạn chế.

  2. Refresh Token:

    • Dùng để xin cấp một Access Token mới khi cái cũ hết hạn mà người dùng không cần nhập lại Mật khẩu.

    • Có thời hạn sống dài hơn (ví dụ: 7 ngày - 30 ngày) và được lưu an toàn (ví dụ: HttpOnly Cookie trên Web hoặc Secure Storage trên App).

Dưới đây là một chương trình Python tự dựng (from scratch) mô phỏng chính xác cơ chế hoạt động của JWT (JSON Web Token) mà không cần dùng thêm bất kỳ thư viện bên thứ ba nào (chỉ sử dụng các thư viện chuẩn của Python như json, hmac, hashlib, base64, time).

Ví dụ này sẽ minh họa 3 bước cốt lõi:

  1. Khởi tạo Token (Sign/Encode) khi đăng nhập thành công.

  2. Xác thực Token (Verify/Decode) khi Client gửi Token lên server.

  3. Mô phỏng 2 trường hợp lỗi: Hacker cố tình sửa đổi dữ liệu và Token bị hết hạn (Expired).

Mã nguồn Python mô phỏng JWT (jwt_demo.py)

 

import base64
import hashlib
import hmac
import json
import time

# Khóa bí mật nằm trên Server (Tuyệt đối không để lộ ra ngoài)
SERVER_SECRET_KEY = "my_super_secret_key_for_course_app"


# --- 1. HÀM TẠO JWT (ENCODE) ---
def create_jwt(payload_data: dict, expires_in_seconds: int = 3600) -> str:
    """Tạo chuỗi JWT gồm 3 phần Header.Payload.Signature"""
    # Bước A: Định nghĩa Header
    header = {"alg": "HS256", "typ": "JWT"}

    # Bước B: Bổ sung thời gian hết hạn (exp) và thời gian tạo (iat) vào Payload
    payload = payload_data.copy()
    now = int(time.time())
    payload["iat"] = now  # Issued At
    payload["exp"] = now + expires_in_seconds  # Expiration Time

    # Bước C: Mã hóa Base64URL cho Header và Payload
    def base64url_encode(data: dict) -> str:
        json_bytes = json.dumps(
            data, separators=(",", ":")
        ).encode()  # Đưa về JSON gọn nhất
        # Base64 băm và xóa các ký tự padding '=' thừa theo chuẩn JWT
        return base64.urlsafe_b64encode(json_bytes).decode().rstrip("=")

    header_b64 = base64url_encode(header)
    payload_b64 = base64url_encode(payload)

    # Bước D: Tạo Chữ ký điện tử Signature = HMAC-SHA256(Header.Payload, SecretKey)
    unsigned_token = f"{header_b64}.{payload_b64}"
    signature_bytes = hmac.new(
        SERVER_SECRET_KEY.encode(), unsigned_token.encode(), hashlib.sha256
    ).digest()

    signature_b64 = base64.urlsafe_b64encode(signature_bytes).decode().rstrip("=")

    # Ghép 3 phần lại bằng dấu chấm
    return f"{header_b64}.{payload_b64}.{signature_b64}"


# --- 2. HÀM XÁC THỰC JWT (VERIFY) ---
def verify_jwt(token: str) -> dict:
    """Giải mã và kiểm tra tính hợp lệ của JWT"""
    try:
        # Bước A: Tách 3 phần của Token
        parts = token.split(".")
        if len(parts) != 3:
            return {"valid": False, "error": "Token sai định dạng (không đủ 3 phần)"}

        header_b64, payload_b64, signature_b64 = parts

        # Bước B: Tính toán lại Chữ ký để kiểm tra xem dữ liệu có bị chỉnh sửa không
        unsigned_token = f"{header_b64}.{payload_b64}"
        expected_signature_bytes = hmac.new(
            SERVER_SECRET_KEY.encode(), unsigned_token.encode(), hashlib.sha256
        ).digest()
        expected_signature_b64 = (
            base64.urlsafe_b64encode(expected_signature_bytes).decode().rstrip("=")
        )

        # So sánh Signature (dùng hmac.compare_digest để chống tấn công Timing Attack)
        if not hmac.compare_digest(signature_b64, expected_signature_b64):
            return {
                "valid": False,
                "error": "❌ CẢNH BÁO: Chữ ký không khớp! Token đã bị hacker can thiệp!",
            }

        # Bước C: Giải mã Payload thu về dict
        # Thêm lại dấu '=' padding nếu bị thiếu khi decode Base64
        padding_needed = 4 - (len(payload_b64) % 4)
        if padding_needed != 4:
            payload_b64_padded = payload_b64 + ("=" * padding_needed)
        else:
            payload_b64_padded = payload_b64

        payload_bytes = base64.urlsafe_b64decode(payload_b64_padded)
        payload = json.loads(payload_bytes.decode())

        # Bước D: Kiểm tra thời gian hết hạn (Expiration Check)
        now = int(time.time())
        if payload.get("exp") and now > payload["exp"]:
            return {"valid": False, "error": "⏳ Token đã hết hạn sử dụng (Expired)"}

        return {"valid": True, "payload": payload}

    except Exception as e:
        return {"valid": False, "error": f"Lỗi giải mã: {str(e)}"}


# =====================================================================
# CHẠY KỊCH BẢN THỬ NGHIỆM
# =====================================================================

print("===== 1. TẠO TOKEN KHI ĐĂNG NHẬP THÀNH CÔNG =====")
user_data = {"user_id": 1024, "username": "tuan_actuary", "role": "admin"}
# Tạo token có hạn dùng trong 2 giây (để test tính năng hết hạn)
token = create_jwt(user_data, expires_in_seconds=2)

print(f"Chuỗi JWT Server cấp cho Client:\n{token}\n")

# Bẻ nhỏ token ra để người dùng quan sát 3 phần
p1, p2, p3 = token.split(".")
print(f" [Header]   : {p1}")
print(f" [Payload]  : {p2}")
print(f" [Signature]: {p3}\n")


print("===== 2. GỬI TOKEN HỢP LỆ VỀ SERVER =====")
result = verify_jwt(token)
if result["valid"]:
    print("✅ Xác thực thành công!")
    print(f"   Dữ liệu giải mã từ Payload: {result['payload']}\n")


print("===== 3. MÔ PHỎNG HACKER THAY ĐỔI DỮ LIỆU PAYLOAD =====")
# Hacker lấy được Token, giải mã phần Payload và sửa "user_id": 1024 thành "user_id": 1
hacker_payload = {"user_id": 1, "username": "tuan_actuary", "role": "admin"}
hacker_payload_b64 = (
    base64.urlsafe_b64encode(json.dumps(hacker_payload).encode()).decode().rstrip("=")
)

# Hacker ghép Payload giả vào Token cũ
tampered_token = f"{p1}.{hacker_payload_b64}.{p3}"
print(f"Token bị chỉnh sửa: {tampered_token}")

result = verify_jwt(tampered_token)
print(f"Kết quả kiểm tra từ Server: {result['error']}\n")


print("===== 4. MÔ PHỎNG TOKEN BỊ HẾT HẠN (EXPIRED) =====")
print("Chờ 3 giây để Token hết hạn...")
time.sleep(3)

result = verify_jwt(token)
print(f"Kết quả kiểm tra từ Server: {result['error']}")

Màn hình Kết quả khi chạy chương trình:

===== 1. TẠO TOKEN KHI ĐĂNG NHẬP THÀNH CÔNG =====
Chuỗi JWT Server cấp cho Client:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMDI0LCJ1c2VybmFtZSI6InR1YW5fYWN0dWFyeSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcyNjk2MjU1OCwiZXhwIjoxNzI2OTYyNTYwfQ.k0y-N0xL_Q7S4e5w93kL...

 [Header]   : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
 [Payload]  : eyJ1c2VyX2lkIjoxMDI0LCJ1c2VybmFtZSI6InR1YW5fYWN0dWFyeSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcyNjk2MjU1OCwiZXhwIjoxNzI2OTYyNTYwfQ
 [Signature]: k0y-N0xL_Q7S4e5w93kL...

===== 2. GỬI TOKEN HỢP LỆ VỀ SERVER =====
✅ Xác thực thành công!
   Dữ liệu giải mã từ Payload: {'user_id': 1024, 'username': 'tuan_actuary', 'role': 'admin', 'iat': 1726962558, 'exp': 1726962560}

===== 3. MÔ PHỎNG HACKER THAY ĐỔI DỮ LIỆU PAYLOAD =====
Token bị chỉnh sửa: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjogMX0.k0y-N0xL_Q7S4e5w93kL...
Kết quả kiểm tra từ Server: ❌ CẢNH BÁO: Chữ ký không khớp! Token đã bị hacker can thiệp!

===== 4. MÔ PHỎNG TOKEN BỊ HẾT HẠN (EXPIRED) =====
Chờ 3 giây để Token hết hạn...
Kết quả kiểm tra từ Server: ⏳ Token đã hết hạn sử dụng (Expired)

 

Điểm mấu chốt công nghệ bạn cần nắm qua ví dụ:

  1. Header & Payload công khai: Bất kỳ ai có Token đều đọc được user_id hay role (vì nó chỉ mã hóa dạng Base64).

  2. Signature bảo vệ toàn vẹn: Hacker có thể sửa user_id thành 1 ở Payload, nhưng vì Hacker không có SERVER_SECRET_KEY nên không thể tạo ra Signature mới hợp lệ. Khi Server tính toán lại Signature sẽ phát hiện điểm vô lý ngay lập tức.