Web Locks API có thể ngăn chặn năm tab đang mở cùng lúc gửi dồn dập các lệnh gọi refresh-token đến máy chủ xác thực, giúp người dùng tránh khỏi việc bị đăng xuất đột ngột. Bằng cách điều phối việc làm mới giữa các tab, một yêu cầu duy nhất sẽ thay thế cho một loạt các yêu cầu vốn thường kích hoạt việc hủy phiên khi cơ chế Refresh Token Rotation được sử dụng.

Sự quá tải tiềm ẩn trong một phiên làm việc đa tab

Một ứng dụng đơn trang (single-page app) điển hình thường thêm một Axios interceptor để theo dõi phản hồi 401, chuyển đổi cờ Boolean isRefreshing, và xếp hàng các yêu cầu đang gửi cho đến khi một JWT mới được trả về. Khi thử nghiệm trên một tab, quy trình này hoạt động hoàn hảo.

Mở cùng một ứng dụng đó trên năm tab, để token truy cập hết hạn, và cả năm tab đều nhận thấy lỗi 401 tại cùng một mili giây. Mỗi tab đều nghĩ rằng nó phải thực hiện làm mới, vì vậy năm yêu cầu refresh-token giống hệt nhau sẽ cùng lúc gửi đến máy chủ xác thực. Với Refresh Token Rotation—một biện pháp bảo mật sẽ vô hiệu hóa refresh token trước đó ngay khi một token mới được cấp—máy chủ sẽ coi yêu cầu thứ hai là một cuộc tấn công phát lại (replay attack), đánh dấu phiên làm việc đã bị xâm phạm và thu hồi nó. Người dùng sẽ bị đăng xuất khỏi mọi tab ngay lập tức.

Nguyên nhân gốc rễ là mô hình cô lập của JavaScript. Một biến như isRefreshing chỉ tồn tại trong tab đã thiết lập nó; các tab khác không có cách nào biết được rằng một quá trình làm mới đang diễn ra. Kết quả là một vấn đề kinh điển về tính đồng thời (concurrency), nhưng các "tiến trình" ở đây là các tab trình duyệt thay vì các luồng (threads).

Tại sao khóa liên tab (cross-tab lock) là công cụ phù hợp

Điều chúng ta cần là một cách để các tab có thể giao tiếp với nhau về một tài nguyên dùng chung—trong trường hợp này là JWT mới. Web Locks API, được cung cấp qua navigator.locks, chính xác là thứ đó. Nó cho phép các script yêu cầu một khóa được đặt tên mà trình duyệt sẽ thực thi trên tất cả các ngữ cảnh (contexts) thuộc cùng một origin. Nếu một khóa đã được giữ, các bên gọi khác sẽ được xếp hàng cho đến khi người giữ khóa giải phóng nó hoặc trình duyệt hủy bỏ nó (ví dụ: khi tab bị lỗi). Không cần máy chủ bên ngoài, không cần polling, chỉ là sự điều phối gốc của trình duyệt.

Triển khai quy trình làm mới dựa trên khóa

  1. Phát hiện lỗi 401 – Axios interceptor bắt được phản hồi không được phép như trước.
  2. Yêu cầu một khóa độc quyền – Tab gọi navigator.locks.request('auth_token_refresh_lock', async lock => { … }). Tại một thời điểm, chỉ có một tab có thể truy cập vào callback.
  3. Kiểm tra lại trước khi gọi mạng – Bên trong khóa, hãy đọc một dấu thời gian (timestamp) (hoặc chính token đó) từ localStorage. Nếu dấu thời gian này mới hơn vài giây, nghĩa là một tab khác đã làm mới token; tab hiện tại sẽ bỏ qua lệnh gọi mạng và chỉ đơn giản là đọc JWT mới từ bộ nhớ.
  4. Làm mới nếu cần – Nếu dấu thời gian được lưu trữ đã cũ, hãy gửi yêu cầu làm mới, lưu token mới và thời gian hiện tại vào localStorage, sau đó giải phóng khóa bằng cách kết thúc callback.
  5. Tiếp tục các yêu cầu đang chờ – Tất cả các tab khác đang chờ sẽ lần lượt giành được khóa, thấy dấu thời gian mới và hoàn tất mà không cần thực hiện thêm yêu cầu nào khác.
async function refreshIfNeeded() {
  await navigator.locks.request('auth_token_refresh_lock', async lock => {
    const lastRefresh = Number(localStorage.getItem('token_refreshed_at') || 0);
    const now = Date.now();
    if (now - lastRefresh < 5_000) return; // another tab already refreshed

    const newToken = await callRefreshEndpoint(); // actual network call
    localStorage.setItem('jwt', newToken);
    localStorage.setItem('token_refreshed_at', now.toString());
  });
}

Mô hình này đảm bảo rằng, bất kể có bao nhiêu tab đang mở, chỉ có một yêu cầu làm mới duy nhất được gửi đến máy chủ.

Những lợi ích có thể đo lường được

  • Hiệu quả mạng – Một yêu cầu thay thế cho năm yêu cầu, giúp giảm đáng kể băng thông và tải cho máy chủ.
  • An toàn phiên làm việc – Với Refresh Token Rotation, máy chủ chỉ thấy một lần sử dụng duy nhất của refresh token cũ, vì vậy nó sẽ không bao giờ đánh dấu phiên làm việc là bị xâm phạm.
  • Khả năng phục hồi – Nếu tab đang giữ khóa bị lỗi, trình duyệt sẽ tự động giải phóng khóa, ngăn chặn tình trạng bế tắc (deadlock) có thể làm đình trệ tất cả các tab.
  • Khả năng mở rộng – Người dùng có thể mở hàng chục tab mà không lo bị đăng xuất hàng loạt, vì việc điều phối diễn ra ngay trong trình duyệt.

Mặt trái: hỗ trợ trình duyệt và phương án dự phòng

Web Locks API là một tính năng tương đối mới. Các trình duyệt dựa trên Chromium hiện đại và các phiên bản gần đây của Firefox đã triển khai nó, nhưng các trình duyệt cũ và Safari thì chưa hỗ trợ gốc. Trong các môi trường mà API này không khả dụng, các nhà phát triển phải chuyển sang một kỹ thuật kém tin cậy hơn—chẳng hạn như phát một sự kiện tùy chỉnh qua localStorage hoặc sử dụng một Shared Worker—để mô phỏng việc báo hiệu liên tab. Những cách khắc phục này thiếu khả năng bảo vệ chống bế tắc tự động mà navigator.locks cung cấp, vì vậy chúng nên được sử dụng một cách thận trọng.

Điều cần theo dõi tiếp theo

  • Tiến độ tiêu chuẩn hóa – Hãy theo dõi biểu đồ mức độ áp dụng của API; sự hỗ trợ rộng rãi hơn sẽ biến phương pháp dựa trên khóa (lock-based) trở thành mặc định cho bất kỳ sự phối hợp giữa các tab nào.
  • Các lớp bao thư viện (Library wrappers) – Một vài tiện ích mã nguồn mở đã và đang trừu tượng hóa mô hình yêu cầu khóa, giúp việc tích hợp vào các Axios interceptors hiện có trở nên dễ dàng hơn.
  • Kiểm tra bảo mật – Mặc dù cơ chế khóa giải quyết được vấn đề tranh chấp (concurrency), endpoint làm mới (refresh endpoint) vẫn phải thực thi việc xoay vòng token (token rotation) và giới hạn tốc độ (rate limiting) một cách hợp lý, vì một tab độc hại duy nhất vẫn có thể làm tràn ngập máy chủ bằng các yêu cầu nếu cơ chế khóa bị vượt qua.

Bài học rút ra rất đơn giản: hãy coi một tập hợp các tab đang mở như một hệ thống phân tán và cung cấp cho chúng một nguyên ngữ đồng bộ hóa (synchronization primitive) gốc. Bằng cách kết nối Web Locks API vào luồng làm mới token, các nhà phát triển sẽ loại bỏ được "bẫy token tab trình duyệt" và giữ cho người dùng luôn ở trạng thái đăng nhập, bất kể họ đang mở bao nhiêu tab cùng lúc.