更新
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import type { ApiResponse } from "../types/api";
|
||||
import { getAccessToken } from "./storage";
|
||||
/**
|
||||
* @description Web 端统一 HTTP 客户端;自动注入 Authorization、401 触发 refresh token 轮换、并发刷新合并、刷新失败跳登录
|
||||
* @filename http.ts
|
||||
* @author wangys
|
||||
* @copyright xyzh
|
||||
* @since 2026-04-17
|
||||
*/
|
||||
import type { ApiResponse } from "@/types";
|
||||
import { clearTokens, getAccessToken, getRefreshToken, setTokens } from "@/utils/storage";
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "/api";
|
||||
const DEFAULT_TIMEOUT = 10000;
|
||||
@@ -11,8 +18,11 @@ interface RequestOptions {
|
||||
body?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
skipAuthRefresh?: boolean;
|
||||
}
|
||||
|
||||
let refreshPromise: Promise<string | null> | null = null;
|
||||
|
||||
function isApiResponse(payload: unknown): payload is ApiResponse<unknown> {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return false;
|
||||
@@ -28,6 +38,10 @@ function buildUrl(path: string) {
|
||||
return `${BASE_URL}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
function isAuthTokenPath(path: string) {
|
||||
return path.startsWith("/auth/tokens");
|
||||
}
|
||||
|
||||
async function parseResponse(response: Response) {
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
@@ -59,6 +73,17 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
|
||||
});
|
||||
|
||||
const payload = await parseResponse(response);
|
||||
if (response.status === 401 && !options.skipAuthRefresh && !isAuthTokenPath(path)) {
|
||||
const nextAccessToken = await tryRefreshAccessToken();
|
||||
if (nextAccessToken) {
|
||||
return request<T>(path, { ...options, skipAuthRefresh: true });
|
||||
}
|
||||
clearTokens();
|
||||
if (typeof window !== "undefined" && window.location.pathname !== "/login") {
|
||||
window.location.replace("/login");
|
||||
}
|
||||
throw new Error("登录已过期,请重新登录");
|
||||
}
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
typeof payload === "object" && payload !== null && "message" in payload
|
||||
@@ -76,6 +101,48 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
|
||||
}
|
||||
}
|
||||
|
||||
async function tryRefreshAccessToken(): Promise<string | null> {
|
||||
if (refreshPromise) {
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
const refreshToken = getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
refreshPromise = (async () => {
|
||||
try {
|
||||
const response = await fetch(buildUrl("/auth/tokens/refresh"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ refreshToken })
|
||||
});
|
||||
const payload = await parseResponse(response);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
if (!isApiResponse(payload) || payload.code !== 0 || !payload.data) {
|
||||
return null;
|
||||
}
|
||||
const tokenData = payload.data as { accessToken?: string; refreshToken?: string };
|
||||
if (!tokenData.accessToken || !tokenData.refreshToken) {
|
||||
return null;
|
||||
}
|
||||
setTokens(tokenData.accessToken, tokenData.refreshToken);
|
||||
return tokenData.accessToken;
|
||||
} catch (error) {
|
||||
return null;
|
||||
} finally {
|
||||
refreshPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
export const http = {
|
||||
get<T>(path: string, options?: Omit<RequestOptions, "method" | "body">) {
|
||||
return request<T>(path, { ...options, method: "GET" });
|
||||
|
||||
2
frontend/src/utils/index.ts
Normal file
2
frontend/src/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./http";
|
||||
export * from "./storage";
|
||||
@@ -1,13 +1,34 @@
|
||||
const ACCESS_TOKEN_KEY = "k12study.access-token";
|
||||
const REFRESH_TOKEN_KEY = "k12study.refresh-token";
|
||||
|
||||
export function getAccessToken() {
|
||||
return localStorage.getItem(ACCESS_TOKEN_KEY);
|
||||
}
|
||||
export function getRefreshToken() {
|
||||
return localStorage.getItem(REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setAccessToken(token: string) {
|
||||
localStorage.setItem(ACCESS_TOKEN_KEY, token);
|
||||
}
|
||||
export function setRefreshToken(token: string) {
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function setTokens(accessToken: string, refreshToken: string) {
|
||||
setAccessToken(accessToken);
|
||||
setRefreshToken(refreshToken);
|
||||
}
|
||||
|
||||
export function clearAccessToken() {
|
||||
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function clearRefreshToken() {
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function clearTokens() {
|
||||
clearAccessToken();
|
||||
clearRefreshToken();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user