Thu Hồi JWT: Giữa hàng triệu người dùng với hàng triệu thiết bị?

Nội dung

Chào các bạn,
Hôm nay mình muốn kể với các bạn một câu chuyện mà mình đã “vật lộn” gần đây khi làm việc với JWT (JSON Web Token). Nếu bạn là một lập trình viên từng đau đầu với việc quản lý phiên đăng nhập trên nhiều thiết bị, chắc chắn bạn sẽ thấy vấn đề này thú vị. Câu hỏi lớn là: Làm sao để thu hồi token trên một thiết bị cụ thể (ví dụ: iPad) mà không ảnh hưởng đến các thiết bị khác (iPhone, Desktop), lại còn không được lưu token trên server, và quan trọng hơn, phải tối ưu hiệu năng khi có hàng triệu người dùng? Nghe đã thấy đau đầu rồi, đúng không? Nhưng đừng lo, mình đã tìm ra một cách hay ho và muốn chia sẻ với các bạn qua bài blog này.

JWT và cơn ác mộng thu hồi

JWT là một “ngôi sao” trong thế giới xác thực vì tính stateless (không trạng thái). Bạn cấp token, client giữ, server chỉ cần kiểm tra chữ ký – đơn giản, nhanh gọn. Nhưng cái gì cũng có giá của nó. Một ngày đẹp trời, sếp bảo mình: “User A đăng nhập trên iPad, iPhone, và Desktop. Giờ họ muốn đăng xuất mỗi iPad thôi, còn lại giữ nguyên. À, không được lưu token trên server nhé!” Mình nghe xong mà muốn khóc – stateless thì làm sao thu hồi được đây?

Cách truyền thống là lưu token vào blacklist trên server, nhưng yêu cầu “không lưu token” đã dập tắt ý tưởng đó. Mình bắt đầu mò mẫm và nhận ra: nếu không thể lưu token, mình phải tìm cách “đánh dấu” thiết bị và để client tự xử lý. Sau vài ngày thử nghiệm (và vài cốc cà phê), mình đã tìm ra một giải pháp: Bloom Filter kết hợp Push Notification. Nghe lạ đúng không? Để mình kể chi tiết nhé.

Ý tưởng: Đánh dấu thiết bị và để client tự kiểm tra

Mình nghĩ thế này: thay vì server phải trả lời từng client rằng “token của mày bị thu hồi chưa?”, mình nhúng thông tin thiết bị vào token và để client tự kiểm tra trạng thái. Nhưng vấn đề là, nếu client cứ liên tục hỏi server, với hàng triệu người dùng thì server sẽ “toang” ngay. Vậy làm sao để tối ưu? Đây là lúc Bloom FilterPush Notification lên sàn.

Bước 1: Nhúng device_id vào JWT

Mỗi khi user đăng nhập, mình cấp token với một trường device_id riêng cho từng thiết bị. Ví dụ:

{
  "sub": "userA",
  "device_id": "ipad-12345",
  "exp": 1710892800
}

iPhone sẽ có device_id: “iphone-54321“, Desktop là desktop-98765. Dễ thôi, đúng không?

Bước 2: Bloom Filter – “Siêu nhân” kiểm tra cục bộ

Bloom Filter là gì? Nói đơn giản, nó như một “danh sách đen siêu nhỏ gọn” mà client có thể tự kiểm tra xem device_id của mình có bị thu hồi không. Nó không lưu chính xác từng token, mà dùng bit để đánh dấu – cực kỳ nhẹ và nhanh. Mình tạo một Bloom Filter trên server, thêm device_id bị thu hồi vào đó, rồi gửi cho client.

Ví dụ, khi user muốn thu hồi iPad:

  • Server thêm “ipad-12345” vào Bloom Filter.
  • Bloom Filter được mã hóa thành base64 (chỉ vài KB) và gửi đi.

Client nhận được Bloom Filter, kiểm tra cục bộ:

  • iPad thấy “ipad-12345” có trong filter → tự đăng xuất.
  • iPhone và Desktop không thấy device_id của mình → yên tâm hoạt động.

Bước 3: Push Notification – Giảm tải server

Thay vì client cứ 5 phút gọi server hỏi “Tao bị thu hồi chưa?“, mình dùng Push Notification (qua Firebase) để gửi Bloom Filter mới mỗi khi có thu hồi. Với hàng triệu người dùng, việc này giảm tải server hàng nghìn lần. Chỉ khi user chủ động thu hồi (rất hiếm), server mới gửi thông báo – quá ngọt!

Ví dụ code: NestJS + Firebase

Mình đã thử implement bằng NestJS và Firebase. Đây là một đoạn code đơn giản để các bạn hình dung:

Server (NestJS)

// auth.service.ts
async revokeDevice(userId: string, deviceId: string): Promise<void> {
  this.bloomFilter.add(deviceId); // Thêm device_id vào Bloom Filter
  await this.sendBloomFilterUpdate(userId); // Gửi qua Push Notification
}

private async sendBloomFilterUpdate(userId: string): Promise<void> {
  const bloomData = Buffer.from(this.bloomFilter.buckets).toString('base64');
  const message = {
    data: { userId, bloomFilter: bloomData },
    topic: `user-${userId}`,
  };
  await firebaseAdmin.messaging().send(message);
}

Client (giả lập)

// client.js
checkRevocation() {
  if (this.bloomFilter.test(this.deviceId)) {
    console.log(`Device ${this.deviceId} bị thu hồi. Đăng xuất đây!`);
    this.logout();
  } else {
    console.log(`Device ${this.deviceId} vẫn ngon lành.`);
  }
}

Chạy thử trên local, mình thấy iPad tự logout khi nhận Bloom Filter mới, còn iPhone với Desktop thì vô tư. Quá đã!

Hiệu năng: Hàng triệu người dùng thì sao?

Nếu mỗi client gọi server 1 lần/phút, với 1 triệu người dùng, server phải xử lý 60 triệu request/giờ – kinh khủng! Nhưng với Bloom Filter + Push:

  • Bloom Filter chỉ vài KB, kiểm tra cục bộ nhanh như chớp.
  • Push Notification chỉ gửi khi có thu hồi (có thể 1-2 lần/ngày/user).
  • Firebase xử lý hàng triệu thông báo dễ như ăn kẹo.

So sánh: từ 60 triệu request/giờ xuống vài triệu request/ngày. Server của mình thở phào nhẹ nhõm!

Cảm nhận cá nhân

Ban đầu mình nghĩ chuyện thu hồi JWT mà không lưu token là bất khả thi, nhưng Bloom Filter đã thay đổi cách nhìn của mình. Nó không chỉ giải quyết vấn đề mà còn dạy mình cách tư duy tối ưu hiệu năng. Nếu bạn đang làm dự án lớn, hãy thử giải pháp này – vừa nhẹ, vừa nhanh, lại còn “xịn”.

Các bạn thấy sao? Có ai từng gặp bài toán tương tự và giải quyết khác không? Comment chia sẻ với mình nhé, mình rất muốn học hỏi thêm từ cộng các bạn!

Happy coding!

Anthony Nguyễn

Cây bút chính tại VietnamTutor

Bài viết cùng chuyên mục

Nâng Cấp Laravel 13: Checklist 10 Bước Không Thể Bỏ Qua 2026

Hướng dẫn nâng cấp Laravel 13 chi tiết với checklist 10 bước. Từ kiểm tra PHP 8.3, cập nhật dependencies, đến xử lý lỗi thường gặp

Hardening Laravel Production: Checklist Bảo Mật Toàn Diện 2026

Checklist hardening Laravel production toàn diện. Từ cấu hình server, database, SSL đến security headers, rate limiting và monitoring.

Authentication & Authorization Trong Laravel: Hướng Dẫn A-Z 2026

Hướng dẫn chi tiết cách xây dựng hệ thống Authentication (xác thực) và Authorization (phân quyền) trong Laravel với Breeze, Fortify, Sanctum, Policies và Gates.

Bảo Mật Laravel: 10 Lỗi Phổ Biến & Cách Phòng 2026

Hướng dẫn 10 lỗi bảo mật phổ biến nhất trong Laravel và cách phòng tránh hiệu quả. Từ XSS, SQL injection đến authentication vulnerabilities.

Migration PHP Attributes Laravel 13: Hướng Dẫn Chi Tiết

Cách chuyển đổi từ protected properties sang PHP Attributes trong Laravel 13 với hướng dẫn từng bước và code examples chi tiết.

Laravel 13 Có Gì Mới? Tổng Hợp Tính Năng Mới 2026

Laravel 13 ra mắt ngày 17/3/2026 với PHP 8.3, PHP Attributes, AI SDK và nhiều cải tiến. Khám phá chi tiết các tính năng mới của

Kubernetes for Beginners 2026: Hướng Dẫn Từ A-Z

Kubernetes (K8s) là nền tảng container orchestration phổ biến nhất hiện nay. Bài hướng dẫn này sẽ giúp bạn hiểu Kubernetes là gì, kiến trúc cơ

Docker Compose Best Practices 2026: 10 Tips Quan Trọng

Docker Compose giúp bạn quản lý multi-container applications dễ dàng hơn. Bài viết này tổng hợp 10 best practices quan trọng nhất để sử dụng Docker

Lỗ hổng RCE (CVE-2025-55182) trên React, Next.js?

Cảnh báo khẩn cấp: React2Shell (CVE-2025-55182) gây RCE nghiêm trọng cho React/Next.js. Nắm cơ chế, dấu hiệu & phòng thủ cấp bách để bảo vệ ứng

Dead-Letter Queue: Giải pháp cứu cánh cho tin nhắn lỗi hệ thống

DLQ là chìa khóa quản lý tin nhắn lỗi hiệu quả trong hệ thống phân tán. Đảm bảo tin nhắn không bị mất, tăng độ tin

Lập trình viên: Xây doanh nghiệp một người, kiếm 10.000 USD/tháng

Lập trình viên: Khám phá khung làm việc để xây dựng doanh nghiệp một người, kiếm 10.000 USD/tháng. Biến kỹ năng code thành cỗ máy tiền,