Web Locks API 可以防止五个打开的标签页同时向认证服务器发送大量的 refresh-token 请求,从而避免用户突然被登出。通过协调各标签页之间的刷新操作,单个请求可以取代通常在启用 Refresh Token Rotation 时会触发会话终止的请求洪流。
多标签页会话中隐藏的过载
典型的单页应用(SPA)会添加一个 Axios 拦截器来监听 401 响应,翻转布尔值 isRefreshing 标志,并对所有传出的请求进行排队,直到新的 JWT 到达。在单个标签页中测试时,这一流程运行完美。
在五个标签页中打开同一个应用,让 access token 过期,所有五个标签页都会在同一毫秒察觉到 401。每个标签页都认为自己必须进行刷新,因此五个完全相同的 refresh-token 请求会竞相冲击认证服务器。由于使用了 Refresh Token Rotation(一种安全机制,在签发新 refresh token 时会立即使之前的 token 失效),服务器会将第二个请求视为重放攻击(replay attack),将该会话标记为已受损并将其撤销。用户会在瞬间从所有标签页中被登出。
根本原因在于 JavaScript 的隔离模型。像 isRefreshing 这样的变量仅存在于设置它的标签页中;其他标签页无法得知刷新操作已经在进行中。其结果是一个经典的并发问题,只不过这里的“进程”是浏览器标签页而非线程。
为什么跨标签页锁是正确的工具
我们需要一种让标签页能够就共享资源(在这种情况下是新的 JWT)进行通信的方式。Web Locks API(通过 navigator.locks 暴露)恰好提供了这种能力。它允许脚本请求一个具名锁,浏览器会在属于同一源的所有上下文中强制执行该锁。如果锁已被占用,其他调用者将被排队,直到持有者释放锁或浏览器终止锁(例如在标签页崩溃时)。无需外部服务器,无需轮询,仅靠浏览器原生的协调即可实现。
实现基于锁的刷新流程
- 检测 401 – 像之前一样,Axios 拦截器捕获未授权响应。
- 请求排他锁 – 标签页调用
navigator.locks.request('auth_token_refresh_lock', async lock => { … })。一次只能有一个标签页进入回调函数。 - 在发起网络请求前进行二次检查 – 在锁内部,从
localStorage中读取时间戳(或 token 本身)。如果时间戳距离现在不足几秒,说明另一个标签页已经刷新了 token;当前标签页跳过网络调用,直接从存储中读取新的 JWT。 - 按需刷新 – 如果存储的时间戳已过期,则发送刷新请求,将新 token 和当前时间存入
localStorage,然后通过从回调函数返回来释放锁。 - 恢复排队的请求 – 所有其他正在等待的标签页会依次获取锁,看到最新的时间戳,然后在不发起额外请求的情况下完成操作。
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());
});
}
该模式保证了无论打开多少个标签页,都只有一个刷新请求到达服务器。
可衡量的收益
- 网络效率 – 一个请求取代五个请求,大幅减少带宽消耗和服务器负载。
- 会话安全性 – 使用 Refresh Token Rotation 时,服务器只会看到旧 refresh token 被使用一次,因此永远不会将会话标记为已受损。
- 韧性 – 如果持有锁的标签页崩溃,浏览器会自动释放锁,防止导致所有标签页停滞的死锁。
- 可扩展性 – 用户可以打开数十个标签页而无需担心级联登出的风险,因为协调工作完全在浏览器内部完成。
反面情况:浏览器支持与回退方案
Web Locks API 是一个相对较新的特性。现代基于 Chromium 的浏览器和最近版本的 Firefox 已经实现了它,但旧版浏览器和 Safari 缺乏原生支持。在 API 不可用的环境中,开发者必须回退到不太可靠的技术——例如通过 localStorage 广播自定义事件或使用 Shared Worker——来模拟跨标签页通信。这些变通方案缺乏 navigator.locks 提供的自动死锁保护,因此应谨慎使用。
下一步关注
- 标准化进展 – 关注该 API 的采用曲线;更广泛的支持将使基于锁的方法成为任何跨标签页协作的默认方式。
- 库封装 – 一些开源工具已经开始对锁请求模式进行抽象,使其更容易接入现有的 Axios 拦截器。
- 安全审计 – 虽然锁解决了并发问题,但刷新端点仍必须强制执行适当的令牌轮换和速率限制,因为如果锁被绕过,单个恶意标签页仍可能向服务器发送大量请求。
结论很简单:将一组打开的标签页视为一个分布式系统,并为它们提供原生的同步原语。通过将 Web Locks API 接入令牌刷新流程,开发者可以消除“浏览器标签页令牌陷阱”,无论用户同时操作多少个标签页,都能保持登录状态。
