diff --git a/packages/filesystem/auth.test.ts b/packages/filesystem/auth.test.ts index ca26aad36..9dc1b0b43 100644 --- a/packages/filesystem/auth.test.ts +++ b/packages/filesystem/auth.test.ts @@ -42,4 +42,30 @@ describe("AuthVerify", () => { await expect(AuthVerify("onedrive")).resolves.toBe("cached-access"); expect(fetchMock).not.toHaveBeenCalled(); }); + + it("concurrent expired token verification should share one refresh request", async () => { + await localStorageDAO.saveValue(key, { + accessToken: "old-access", + refreshToken: "old-refresh", + createtime: Date.now() - 3600000 - 1000, + }); + + const fetchMock = vi.fn().mockResolvedValue({ + json: vi.fn().mockResolvedValue({ + code: 0, + data: { + token: { + access_token: "new-access", + refresh_token: "new-refresh", + }, + }, + }), + } as unknown as Response); + vi.stubGlobal("fetch", fetchMock); + + await expect( + Promise.all([AuthVerify("onedrive"), AuthVerify("onedrive"), AuthVerify("onedrive")]) + ).resolves.toEqual(["new-access", "new-access", "new-access"]); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/filesystem/auth.ts b/packages/filesystem/auth.ts index 603fd409b..dd4def31f 100644 --- a/packages/filesystem/auth.ts +++ b/packages/filesystem/auth.ts @@ -73,6 +73,47 @@ export type Token = { refreshToken: string; createtime: number; }; +const refreshTokenPromises: Partial>> = {}; + +function refreshAccessToken( + netDiskType: NetDiskType, + token: Token, + invalid: boolean | undefined, + key: string, + localStorageDAO: LocalStorageDAO +) { + if (refreshTokenPromises[netDiskType]) { + return refreshTokenPromises[netDiskType]; + } + + const refreshPromiseFn = async () => { + const resp = await RefreshToken(netDiskType, token.refreshToken); + if (resp.code !== 0) { + await localStorageDAO.delete(key); + // 刷新失败,并且标记失效,尝试重新获取token + if (invalid) { + return await AuthVerify(netDiskType); + } + throw new WarpTokenError(new Error(resp.msg)); + } + const newToken = { + accessToken: resp.data.token.access_token, + refreshToken: resp.data.token.refresh_token, + createtime: Date.now(), + }; + // 更新token + await localStorageDAO.saveValue(key, newToken); + return newToken.accessToken; + }; + const refreshPromise: Promise = refreshPromiseFn().finally(() => { + if (refreshTokenPromises[netDiskType] === refreshPromise) { + delete refreshTokenPromises[netDiskType]; + } + }); + + refreshTokenPromises[netDiskType] = refreshPromise; + return refreshPromise; +} export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) { let token: Token | undefined = undefined; @@ -99,36 +140,16 @@ export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) { invalid = false; await localStorageDAO.saveValue(key, token); } - // token过期或者失效 - const expired = Date.now() >= token.createtime + 3600000; - if (expired || invalid) { - // 大于一小时刷新token - try { - const resp = await RefreshToken(netDiskType, token.refreshToken); - if (resp.code !== 0) { - await localStorageDAO.delete(key); - // 刷新失败,并且标记失效,尝试重新获取token - if (invalid) { - return await AuthVerify(netDiskType); - } - throw new WarpTokenError(new Error(resp.msg)); - } - token = { - accessToken: resp.data.token.access_token, - refreshToken: resp.data.token.refresh_token, - createtime: Date.now(), - }; - // 更新token - await localStorageDAO.saveValue(key, token); - } catch (e) { - // 已过期或已被服务端判定失效的 token 不能继续回退使用 - console.warn(e); - throw e; - } - } else { - return token.accessToken; + // token未过期(一小时内)及有效则保留,不用刷新token + const unexpired = Date.now() < token.createtime + 3600000; + if (unexpired && !invalid) return token.accessToken; + try { + return await refreshAccessToken(netDiskType, token, invalid, key, localStorageDAO); + } catch (e) { + // 已过期或已被服务端判定失效的 token 不能继续回退使用 + console.warn(e); + throw e; } - return token.accessToken; } export const netDiskTypeMap: Partial> = {