在单页应用中防止令牌刷新竞争条件
竞争条件的产生方式
仪表板在加载时触发五个并发 API 调用是正常的。如果访问令牌在这一刻恰好过期,所有五个调用都会收到 401 响应。每个调用的错误处理程序都会尝试刷新令牌,从而产生五个并发的刷新请求。其中第一个成功,其余四个使旧令牌失效或被刷新端点拒绝。这四个原始请求带着无效令牌重试,并再次失败。用户被登出。
单例 Promise 模式
解决方案是确保同一时间只运行一个刷新操作:
let refreshPromise: Promise<string> | null = null
async function refreshToken(): Promise<string> {
if (\!refreshPromise) {
refreshPromise = doRefresh().finally(() => {
refreshPromise = null
})
}
return refreshPromise
}第一个调用创建 Promise 并存储引用。后续调用获得相同的 Promise,因此它们都等待同一次刷新操作完成,然后继续携带新令牌。
拦截器队列方法
对于使用 axios 或 fetch 封装器的应用,拦截器队列方法提供了更系统化的实现:
HTTP 客户端的响应拦截器检测到 401 时,将请求添加到队列并触发单例刷新。刷新完成后,队列中的所有请求都使用新令牌重新执行。
在 Supabase 中的具体表现
Supabase 客户端内置了令牌刷新逻辑,但在自定义 HTTP 客户端绕过 Supabase 客户端直接调用 API 时,保护机制就会失效。
最可靠的方法是始终通过 Supabase 客户端方法进行认证 API 调用,让客户端处理令牌管理。只有在需要对底层 HTTP 层进行精细控制时,才需要自定义刷新逻辑。
测试竞争条件
竞争条件在集成测试中出了名的难以重现。最有效的方法是:模拟一个在第一次调用时总是返回 401 的认证端点,然后触发多个并发请求,验证只发生了一次刷新调用。