Cạm bẫy token đã xảy ra trên một trang web đang hoạt động khi năm tab được mở bởi cùng một người dùng đều cố gắng làm mới một JWT đã hết hạn vào cùng một mili giây, làm tràn ngập backend bằng các yêu cầu làm mới trùng lặp và ngay lập tức làm mất hiệu lực của phiên làm việc. Mọi tab đều đăng xuất người dùng, chứng minh rằng giải pháp chỉ áp dụng cho một tab để làm mới token không còn đủ nữa.

Tại sao cạm bẫy token lại quan trọng

Các ứng dụng single-page hiện đại sử dụng access token có thời hạn ngắn và refresh token có thời hạn dài. Khi access token hết hạn, client sẽ gửi một yêu cầu làm mới, nhận một cặp token mới và thử lại cuộc gọi ban đầu. Hầu hết các nhà phát triển bảo vệ luồng này bằng một cờ trong bộ nhớ (ví dụ: isRefreshing = true) hoặc một hàng đợi yêu cầu (request queue), và chỉ kiểm thử nó trên một tab duy nhất. Trong thực tế, người dùng thường mở nhiều tab cùng lúc: một trang cài đặt, một bảng điều khiển phân tích và một vài chế độ xem dữ liệu. Khi access token hết hạn, mỗi tab sẽ phát hiện lỗi 401 một cách độc lập, mỗi tab lại gửi một yêu cầu làm mới, và backend—đặc biệt là khi áp dụng cơ chế xoay vòng refresh token (refresh-token rotation)—sẽ coi yêu cầu thứ hai là một hành vi replay và thu hồi toàn bộ phiên làm việc.

Sự cô lập của JavaScript tạo ra vấn đề

Mỗi tab trình duyệt chạy một ngữ cảnh JavaScript riêng. Các biến, bộ hẹn giờ và các cờ trong bộ nhớ đều không thể nhìn thấy bởi các tab khác, ngay cả khi chúng chia sẻ cùng một origin. Một cờ thông báo rằng “việc làm mới đang được thực hiện” chỉ tồn tại bên trong tab đã thiết lập nó. Các tab khác không có cách nào để biết rằng token đang được làm mới ở nơi khác, vì vậy tất cả chúng đều khởi chạy các cuộc gọi mạng của riêng mình. Vấn đề không phải là lỗi trong interceptor; đó là một hạn chế cơ bản của sự cô lập trạng thái ở phía client.

Web Locks API giải cứu tình thế

Khi một tab đang giữ một lock, bất kỳ tab nào khác yêu cầu cùng một lock đó đều phải chờ cho đến khi nó được giải phóng.

Cách thức hoạt động đối với việc làm mới token

  1. Phát hiện lỗi 401 – Bất kỳ tab nào nhận được phản hồi không được phép (unauthorized) sẽ gọi navigator.locks.request('auth_token_refresh_lock', async lock => { … }).
  2. Chiếm giữ lock – Nếu không có tab nào khác đang giữ lock, tab hiện tại sẽ tiếp tục; nếu không, nó sẽ tạm dừng cho đến khi lock được giải phóng.
  3. Làm mới một lần duy nhất – Tab đang giữ lock sẽ gửi yêu cầu làm mới, lưu access token mới và một dấu thời gian (timestamp) vào localStorage, sau đó tự động giải phóng lock khi hàm callback kết thúc.
  4. Bỏ qua các công việc trùng lặp – Khi một tab đang chờ cuối cùng cũng nhận được lock, nó sẽ đọc timestamp từ localStorage. Nếu token đã được làm mới trong một khoảng thời gian có thể cấu hình (ví dụ: vài giây trước), tab đó sẽ bỏ qua cuộc gọi mạng và cập nhật token trong bộ nhớ từ localStorage.
  5. Xử lý sự cố – Nếu một tab bị sập hoặc bị đóng trong khi đang giữ lock, trình duyệt sẽ giải phóng lock, cho phép tab khác thử lại việc làm mới.

Lợi ích tóm tắt

  • Không có các cuộc gọi mạng dư thừa – Chỉ có tab đầu tiên giao tiếp với backend.
  • Không làm mất phiên làm việc – Cơ chế xoay vòng refresh token chỉ ghi nhận một lần sử dụng, giúp duy trì phiên làm việc.
  • Khôi phục mượt mà – Việc giải phóng lock do trình duyệt quản lý giúp ngăn chặn tình trạng deadlock nếu một tab biến mất.

Danh sách kiểm tra triển khai

  • Bao bọc logic làm mới trong interceptor Axios (hoặc fetch) của bạn bằng một yêu cầu lock.
  • Lưu token đã được làm mới và một timestamp tính bằng mili giây vào localStorage (hoặc sessionStorage nếu bạn muốn dữ liệu theo từng phiên).
  • Khi được cấp lock, hãy so sánh timestamp đã lưu với Date.now(). Nếu sự chênh lệch thấp hơn ngưỡng của bạn, hãy đọc token từ bộ lưu trữ thay vì gọi backend.
  • Đảm bảo interceptor cập nhật các header của yêu cầu với token được lấy từ bộ lưu trữ trước khi thử lại cuộc gọi API ban đầu.
  • Kiểm thử luồng này với nhiều tab, mô phỏng độ trễ mạng để xác minh rằng chỉ có duy nhất một yêu cầu làm mới đến được máy chủ.

Những gì có thể sai sót

Những điều cần lưu ý tiếp theo

Bài học rút ra

Hãy coi mỗi tab trình duyệt như một nút (node) trong một hệ thống phân tán nhỏ. Bằng cách sử dụng Web Locks API để tuần tự hóa việc làm mới JWT, bạn sẽ loại bỏ các cuộc gọi trùng lặp, bảo vệ cơ chế xoay vòng refresh token và giữ cho người dùng luôn đăng nhập trên tất cả các tab đang mở của họ.