JWT trong thực tế: Tại sao các triển khai cơ bản thường thất bại
Năm ngoái, đội ngũ của tôi đã chuyển đổi hạ tầng API cốt lõi sang kiến trúc microservices không lưu trạng thái (stateless). Chúng tôi chọn JSON Web Tokens (JWT) vì những lý do quen thuộc: chúng mở rộng dễ dàng và loại bỏ việc truy vấn cơ sở dữ liệu tốn kém cho mỗi yêu cầu. Tuy nhiên, sáu tháng vận hành thực tế đã dạy chúng tôi rằng thiết lập ‘tiêu chuẩn’ thường là mục tiêu dễ dàng cho những kẻ tấn công.
Bảo mật trở thành vấn đề cá nhân đối với tôi sau khi một cuộc tấn công brute-force SSH đánh vào máy chủ riêng vào nửa đêm, ghi nhận hơn 1.200 lần thử thất bại trong chưa đầy một giờ. Sự cố đó đã thay đổi trọng tâm của tôi từ việc chỉ làm cho chức năng hoạt động sang việc củng cố bảo mật mạnh mẽ ở mọi lớp. Với JWT, nhiều nhà phát triển coi cấu hình là việc ‘thiết lập một lần rồi thôi’. Thực tế, nếu không được củng cố, bạn không chỉ để lại chìa khóa dưới thảm—mà về cơ bản là đang công khai bề mặt tấn công cho bất kỳ ai có công cụ debug.
Xây dựng nền tảng vững chắc
Bảo mật đáng tin cậy bắt đầu từ các thư viện phụ thuộc đã được kiểm chứng. Chúng tôi sử dụng PyJWT cho các dịch vụ Python và jsonwebtoken for Node.js. Đừng cố tự viết logic phân tích (parsing) riêng; mật mã học là lĩnh vực mà những sơ suất nhỏ có thể dẫn đến rò rỉ dữ liệu nghiêm trọng.
Trong môi trường Python, chúng tôi cô lập việc thiết lập trong một môi trường ảo (virtual environment) và đảm bảo phiên bản hỗ trợ cryptography được cài đặt:
bash
pip install "PyJWT[crypto]"
Đối với các microservices Node.js, việc cài đặt diễn ra theo tiêu chuẩn:
bash
npm install jsonwebtoken
Cài đặt thư viện mới chỉ là bước đầu tiên. Chúng tôi tích hợp pip-audit và npm audit trực tiếp vào luồng CI/CD. Điều này giúp phát hiện các lỗi bảo mật (CVE) trước khi chúng kịp đi vào container. Nếu thư viện cốt lõi của bạn có lỗ hổng, ngay cả những dòng code tinh tế nhất cũng không cứu vãn được dữ liệu.
Loại bỏ các lỗi cấu hình phổ biến
Hầu hết các vụ rò rỉ JWT xảy ra do các lựa chọn cấu hình ‘tạm thời’ nhưng vô tình vẫn tồn tại khi đưa lên môi trường thực tế. Chúng tôi đã cấu trúc lại cấu hình của mình để xử lý các mối đe dọa thực tế theo mặc định.
1. Loại bỏ thuật toán “None”
Lỗ hổng thuật toán ‘none’ khét tiếng cho phép kẻ tấn công vượt qua chữ ký bằng cách đặt header thành {"alg": "none"}. Nếu hệ thống backend của bạn không hạn chế điều này một cách rõ ràng, nó có thể coi một token không có chữ ký là hợp lệ. Hiện tại chúng tôi đã hardcode các thuật toán được phép trực tiếp vào logic xác thực để ngăn chặn việc thay đổi này.
python
import jwt
# Xác định rõ ràng RS256 để chặn các cuộc tấn công hạ cấp về 'none' hoặc 'HS256'
try:
payload = jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"])
except jwt.InvalidTokenError:
# Ghi log và hủy yêu cầu
pass
2. Chuyển sang thuật toán bất đối xứng RS256
Ban đầu chúng tôi sử dụng HS256 (Đối xứng), yêu cầu mọi microservice phải dùng chung một khóa bí mật (secret key). Điều này tạo ra một rủi ro lây lan lớn: nếu một dịch vụ bị xâm nhập, kẻ tấn công có thể giả mạo token cho toàn bộ hệ thống. Chúng tôi đã chuyển sang RS256, sử dụng khóa riêng (private key) để ký (chỉ lưu tại dịch vụ Auth) và khóa công khai (public key) để xác thực trên 12 dịch vụ còn lại.
bash
# Tạo private key 2048-bit
openssl genrsa -out private.pem 2048
# Trích xuất public key để phân phối cho các microservices
openssl rsa -in private.pem -pubout -out public.pem
3. Tối ưu hóa các Claim trong Payload
Các token trong môi trường thực tế của chúng tôi sử dụng bốn claim bắt buộc: iss (người phát hành), exp (thời gian hết hạn), iat (thời điểm phát hành), và jti (JWT ID). Chúng tôi đặt thời gian hết hạn nghiêm ngặt là 15 phút. Các token có thời hạn dài là một rủi ro lớn. Để giữ người dùng luôn đăng nhập, chúng tôi sử dụng các JWT ngắn hạn này cùng với các refresh token bảo mật, chỉ dùng qua HTTP (HTTP-only) và được xoay vòng (rotate) mỗi khi có yêu cầu access token mới.
Phòng thủ chủ động: Giám sát và Thu hồi
Việc xác thực không kết thúc khi mã nguồn được triển khai. Chúng tôi sử dụng nhật ký (log) để xác định các mẫu hành vi độc hại trước khi chúng trở thành các vụ rò rỉ toàn diện. Trong một đợt kiểm tra gần đây, chúng tôi thấy rằng việc giám sát các bất thường cụ thể đã cho phép chặn đứng một cuộc tấn công credential stuffing chỉ trong vài phút.
Phát hiện các bất thường về chữ ký
Chúng tôi theo dõi các lỗi Signature Verification Failed trên bảng điều khiển. Một vài lỗi là bình thường (tab trình duyệt hết hạn hoặc lỗi phía client), nhưng một sự gia tăng đột biến—chẳng hạn như hơn 50 lỗi từ một IP duy nhất trong một phút—sẽ kích hoạt việc chặn tạm thời tự động. Điều này thường cho thấy ai đó đang thử nghiệm thay đổi thuật toán hoặc cố gắng brute-force khóa bí mật HS256.
Danh sách đen hỗ trợ bởi Redis
JWT là stateless (không lưu trạng thái), khiến việc đăng xuất ngay lập tức trở nên khó khăn. Để khắc phục điều này, chúng tôi sử dụng claim jti (JWT ID) và một danh sách thu hồi được hỗ trợ bởi Redis. Khi người dùng nhấn ‘đăng xuất’, chúng tôi lưu jti đó vào Redis với thời gian sống (TTL) khớp với thời gian còn lại của token. Điều này đảm bảo token vô tác dụng ngay lập tức mà không buộc phải kiểm tra cơ sở dữ liệu cho mỗi yêu cầu đơn lẻ.
python
def is_token_revoked(payload):
jti = payload.get("jti")
# Tra cứu O(1) trong Redis giúp chi phí xác thực duy trì dưới 2ms
return redis_client.exists(f"revoked_token:{jti}")
Lời kết
Bảo mật JWT không phải là về một thiết lập ‘thần thánh’ duy nhất. Đó là một hệ thống phòng thủ nhiều lớp. Bằng cách kết hợp RS256, danh sách trắng thuật toán nghiêm ngặt và giám sát Redis chủ động, chúng tôi đã vận hành các API thực tế trong nửa năm mà không gặp một sự cố xâm nhập xác thực nào. Hãy đối xử với các token của bạn với cùng một sự cẩn trọng như đối với các khóa SSH riêng tư, và API của bạn sẽ trở thành một mục tiêu khó tấn công hơn đáng kể.

