+5

Lột trần sự thật về JWT (Phần 2): Trận chiến Microservices và nỗi ám ảnh "Token Béo Phì"

Tiếp nối Phần 1: Sau khi đã giải quyết xong bài toán lưu trữ và bảo mật JWT trên trình duyệt người dùng, hệ thống của bạn tiếp tục lớn lên. Chào mừng đến với thế giới của Microservices – nơi mọi thứ đơn giản đều trở nên phức tạp gấp mười lần.

Nếu các bạn chưa đọc phần 1 thì link đây nhé: Lột trần sự thật về JWT (Phần 1): Cú lừa "Stateless" và nỗi đau Đăng Xuất

Chương 3: Trận chiến ở quy mô hệ thống

9. Xác thực ở đâu: Trạm gác API Gateway hay từng Microservice?

Khi hệ thống phình to và bị băm nhỏ thành hàng chục service khác nhau (Service Đơn hàng, Service Thanh toán, Service Vận chuyển...), bài toán đau đầu nhất của các Tech Lead và Solution Architect (SA) là: Ai sẽ là người chịu trách nhiệm đọc và kiểm tra chữ ký của chiếc JWT mà người dùng gửi lên?

Lúc này, trên bàn cân thiết kế sẽ xuất hiện hai trường phái đối lập nhau:

Mô hình 1: Xác thực tập trung tại cổng (Centralized Authentication)

Trong mô hình này, API Gateway đóng vai trò như một trạm gác an ninh duy nhất của toàn bộ hệ thống. Khi request chứa JWT đi đến cổng, Gateway sẽ đích thân giải mã, kiểm tra chữ ký (Signature) và xem token có hết hạn hay chưa. Nếu hợp lệ, Gateway sẽ bóc lấy các thông tin quan trọng trong Payload (như User ID, Role), nhét chúng vào một HTTP Header thông thường (ví dụ: X-User-Id: 123) và đẩy sâu vào cho các Microservices bên trong xử lý.

  • Ưu điểm: Hệ thống chạy cực kỳ nhanh. Các Microservices bên trong rũ bỏ được hoàn toàn gánh nặng phải tính toán thuật toán mật mã (tiết kiệm CPU). Việc bảo mật Khóa bí mật (Secret Key) cũng rất nhàn vì chỉ có duy nhất con Gateway cầm nó.

  • Điểm yếu chí mạng: Mô hình này chứa đựng một lỗ hổng kinh điển mang tên "Niềm tin ngây thơ vào mạng nội bộ" (Trusted Internal Network). Hãy tưởng tượng một hacker khai thác được lỗ hổng và chui lọt vào bên trong mạng LAN của bạn. Hắn chỉ cần tự tạo ra một request kẹp sẵn header X-User-Id: 1 (ID của Giám đốc) và bắn thẳng vào Service Thanh toán (đi vòng qua Gateway). Service Thanh toán lúc này hoàn toàn ngây thơ, tin tưởng tuyệt đối vào cái header giả mạo kia và thực hiện lệnh chuyển tiền.

Mô hình 2: Xác thực phân tán (Decentralized / Zero Trust)

Để vá lỗ hổng chí mạng trên, giới kiến trúc sư phải áp dụng triết lý "Không tin bất kỳ ai" (Zero Trust). Lúc này, API Gateway bị tước quyền kiểm tra JWT, nó chỉ làm đúng nhiệm vụ dẫn đường (Routing). Chiếc JWT nguyên bản sẽ được đẩy thẳng xuống tận các Microservices. Bất kỳ service nào nhận được request cũng phải tự mình dùng Khóa toán học để kiểm tra lại chữ ký của token.

Dù hacker có chui được vào mạng nội bộ, hắn cũng không thể tự tạo ra một JWT giả mạo vì hắn không có Khóa để ký. Tuy nhiên, mô hình Zero Trust này lại đẻ ra một nỗi đau khác: Làm sao để phân phát Khóa bảo mật cho 50 cái Microservices cùng lúc một cách an toàn?

👉️Giải pháp của Kiến trúc sư: JWKS và Nghệ thuật Caching

Chúng ta không thể dùng một Khóa bí mật (Symmetric Key - HS256) copy cho 50 services được, vì lộ một cái là mất tất cả. Thay vào đó, chúng ta dùng Cặp khóa Bất đối xứng (Asymmetric Key - RS256). Máy chủ xác thực (Auth Server) giữ Private Key để tạo JWT, còn 50 Microservices sẽ được phát Public Key để tự kiểm tra.

Cơ chế này được vận hành tự động thông qua JWKS (JSON Web Key Set):

  1. Auth Server mở một API công khai (thường là /.well-known/jwks.json) chứa danh sách các Public Key hợp lệ.

  2. Mỗi chiếc JWT sinh ra sẽ được gắn một mã định danh kid (Key ID) trên phần Header.

  3. Khi Microservice nhận JWT, nó đọc mã kid và biết phải dùng Public Key nào để verify.

Nhưng để tránh việc các Microservices tự DDoS Auth Server bằng cách gọi mạng liên tục để xin khóa, các hệ thống bắt buộc phải áp dụng Caching: Microservice sẽ tải danh sách JWKS một lần lúc khởi động và lưu thẳng vào RAM. Mọi quá trình xác thực sau đó diễn ra với tốc độ ánh sáng ngay trên RAM. Chỉ khi nào có một token mang mã kid mới lạ xuất hiện (do Auth Server vừa đổi khóa bảo mật định kỳ), Microservice mới chủ động gọi mạng đi tải file JWKS mới nhất về cập nhật.

10. Nút thắt cổ chai mang tên "Payload Bloat" (Token béo phì)

Giữ được tính Stateless và Zero Trust cho Microservices là một thành tựu lớn, nhưng nó mang lại một tác dụng phụ khủng khiếp. Vì muốn các service không phải gọi chéo nhau hay chọc vào Database để hỏi quyền hạn, các lập trình viên bắt đầu nhét "cả thế giới" vào Payload của JWT.

Từ User ID, Email, cho đến một mảng (array) dài dằng dặc chứa hàng chục Roles và Permissions. Hậu quả là từ một token nhẹ nhàng, nó phình to thành một khối dữ liệu nặng 4KB.

Hãy làm một phép toán mạng lưới: Nếu trang chủ của bạn cần gọi song song 20 API requests. Mỗi request đều bị ép phải cõng theo chiếc JWT 4KB đó trong Header. Tổng cộng bạn vừa đốt 80KB băng thông chỉ để... chứng minh người dùng là ai, trước cả khi một byte dữ liệu nghiệp vụ nào thực sự được truyền đi. Ở quy mô hàng chục ngàn người dùng truy cập cùng lúc, "Token béo phì" sẽ bóp nghẹt hoàn toàn băng thông hệ thống của bạn.

11. Tuyệt kỹ cứu rỗi băng thông: "Phantom Token Pattern"

Làm sao để Client (Trình duyệt/Mobile App) ở bên ngoài Internet không phải tải cục data 4KB, nhưng các Microservices ở bên trong mạng nội bộ vẫn nhận được cục data 4KB đó để xác thực Stateless? Lời giải nằm ở một tuyệt kỹ kiến trúc mang tên Phantom Token Pattern (Token Bóng ma).

Cách vận hành của Phantom Token tinh tế như một màn ảo thuật:

  1. Thế giới bên ngoài cầm Opaque Token: Khi người dùng đăng nhập thành công, Auth Server vẫn tính toán và tạo ra chiếc JWT 4KB khổng lồ. Nhưng khoan đã, nó KHÔNG gửi chiếc JWT này về cho trình duyệt. Thay vào đó, nó cất JWT vào một In-memory Database siêu tốc (như Redis), và chỉ tạo ra một chuỗi ngẫu nhiên, vô nghĩa, cực ngắn (ví dụ một mã UUID: 7b9a-4c2f...) để gửi về cho Client. Đây gọi là Opaque Token.

  2. Cánh cửa ma thuật (API Gateway): Ở những lần gọi API tiếp theo, Client chỉ mang chuỗi UUID siêu nhẹ này gửi lên. Khi request chạm tới API Gateway, Gateway sẽ lấy chuỗi UUID đó làm chìa khóa (Key) chọc vào Redis để "chuộc" lại chiếc JWT 4KB nguyên bản (Value).

  3. Thế giới bên trong hoàn toàn Stateless: Gateway bốc chiếc JWT 4KB đính kèm vào request và đẩy sâu vào mạng lưới Microservices. Các service bên trong cứ thế dùng JWKS trên RAM để kiểm tra chữ ký một cách độc lập.

Đòn kết liễu cho bài toán Logout hoàn hảo

Bạn còn nhớ sự bất lực khi cố gắng thu hồi (Revoke) một JWT chưa hết hạn ở Phần 1 chứ? Với Phantom Token, bài toán đau đầu nhất lịch sử này được giải quyết êm đẹp chỉ bằng một câu lệnh.

Khi người dùng bấm "Logout", API Gateway chỉ cần bắn một lệnh duy nhất vào Redis: DEL <chuỗi_UUID>.

Bản ghi trong Redis bị bốc hơi ngay lập tức. Ở request tiếp theo, dù Client vẫn cầm chuỗi UUID cũ gửi lên, nhưng Gateway tìm trong Redis trả về null. Gateway lập tức chặn đứng request bằng lỗi 401 Unauthorized. Phiên đăng nhập bị tiêu diệt tức thì, dứt khoát, mà chúng ta không cần mảy may quan tâm chiếc JWT 4KB kia bao giờ mới hết hạn.

Chương 4: Góc nhìn Kiến trúc sư và Quyết định "Xuống tiền"

Dưới góc độ của một Solution Architect, không có công nghệ nào là "viên đạn bạc" (Silver Bullet). Mọi quyết định kiến trúc đều là một cuộc trao đổi (Trade-off).

12. Khi nào tuyệt đối KHÔNG nên dùng JWT? (Cái bẫy Over-engineering)

Nếu hệ thống của bạn rơi vào các kịch bản sau, hãy dũng cảm quay về với Session truyền thống:

  • Hệ thống Monolith (Một server duy nhất): RAM của server dư sức chứa hàng trăm ngàn Session. Đừng rước sự phức tạp của JWT (Refresh Token, Cookie Security, Caching JWKS) vào dự án chỉ vì "nghe nói nó là trend". Code xong dự án có khi bạn mất luôn thanh xuân chỉ để fix bug bảo mật.

  • Hệ thống có quyền hạn (Permissions) thay đổi liên tục: Nếu sếp yêu cầu tước quyền của một user và nó phải có hiệu lực ngay từng giây, Session làm việc này xuất sắc vì mọi thứ check trực tiếp dưới DB. Với JWT, token đã phát hành thì không thể sửa Payload được nữa.

  • Ứng dụng Tài chính/Ngân hàng lõi (Core Banking): Những hệ thống này đòi hỏi khả năng kiểm soát trạng thái tuyệt đối. Họ cần quyền "giết" một phiên giao dịch ngay lập tức khi phát hiện rủi ro, nên mô hình Stateful Session luôn là ưu tiên số một.

13. Vậy JWT thực sự tỏa sáng ở đâu? (Khi nào NÊN dùng)

JWT không sinh ra để thay thế Session. Nó sinh ra để giải quyết bài toán của Sự phân tán và Ủy quyền:

  • Hệ thống Microservices quy mô lớn: Nơi mà hàng chục services cần biết "Ai đang gọi tôi" bằng mô hình Zero Trust + Phantom Token mà không làm sập Database tập trung.

  • Hệ sinh thái SSO (Single Sign-On) & OAuth2/OIDC: Khi bạn bấm "Login with Google" vào Spotify, Google sẽ cấp cho Spotify một chiếc JWT. Spotify tự mình kiểm tra chữ ký của Google để cho bạn nghe nhạc mà không cần dùng chung Database với Google. Đây là sân chơi vô đối của JWT.

14. Lời kết: Công cụ, không phải chân lý

Chúng ta đã đi một chặng đường rất dài: từ những lầm tưởng ngây thơ về Base64, sự thật về chữ ký toán học, cuộc chiến chống XSS/CSRF trên trình duyệt, cho đến những màn thiết kế kiến trúc hóc búa để phân phát Khóa (JWKS) và thu hồi Token (Phantom Token) trong hệ thống Microservices khổng lồ.

Bài học lớn nhất đọng lại là gì? Đừng dùng JWT chỉ vì nó hợp xu hướng. Trước khi áp dụng nó vào dự án, hãy tự hỏi: Hệ thống của mình có thực sự cần đến sự Stateless không? Mình đã sẵn sàng đánh đổi sự đơn giản của Session để lấy khả năng mở rộng (Scale) chưa? Khi trả lời được những câu hỏi đó một cách thấu đáo, bạn đã không còn là một lập trình viên chỉ biết gõ code theo các bài Tutorial nữa. Bạn đã tư duy như một Kiến trúc sư hệ thống thực thụ.

Chúc các bạn áp dụng JWT một cách tỉnh táo và thành công trong các dự án sắp tới!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí