Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/filesystem/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
79 changes: 50 additions & 29 deletions packages/filesystem/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,47 @@ export type Token = {
refreshToken: string;
createtime: number;
};
const refreshTokenPromises: Partial<Record<NetDiskType, Promise<string>>> = {};

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<string> = 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;
Expand All @@ -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<Record<FileSystemType, NetDiskType>> = {
Expand Down
Loading