From 6979a175ee93ace0faa82451dbd0a6ebf4689f44 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Tue, 23 Sep 2025 15:55:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor(auth):=20:recycle:=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E8=AE=A4=E8=AF=81=E9=80=BB=E8=BE=91=EF=BC=8C=E5=88=86?= =?UTF-8?q?=E7=A6=BB=E7=99=BB=E5=BD=95=E8=B7=B3=E8=BD=AC=E4=B8=8Etoken?= =?UTF-8?q?=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/auth/useTokenRefresh.ts | 87 ++++++++++++++ src/composables/index.ts | 1 + src/settings.ts | 15 +++ src/utils/auth.ts | 24 ++++ src/utils/request.ts | 107 +++--------------- ...n-column.vue => auto-operation-column.vue} | 0 .../demo/{curd-demo.vue => curd-single.vue} | 0 .../{icon-selector.vue => icon-select.vue} | 0 8 files changed, 144 insertions(+), 90 deletions(-) create mode 100644 src/composables/auth/useTokenRefresh.ts rename src/views/demo/{auto-opreation-column.vue => auto-operation-column.vue} (100%) rename src/views/demo/{curd-demo.vue => curd-single.vue} (100%) rename src/views/demo/{icon-selector.vue => icon-select.vue} (100%) diff --git a/src/composables/auth/useTokenRefresh.ts b/src/composables/auth/useTokenRefresh.ts new file mode 100644 index 00000000..c81be723 --- /dev/null +++ b/src/composables/auth/useTokenRefresh.ts @@ -0,0 +1,87 @@ +import type { InternalAxiosRequestConfig } from "axios"; +import { useUserStoreHook } from "@/store/modules/user-store"; +import { AuthStorage, redirectToLogin } from "@/utils/auth"; + +/** + * 重试请求的回调函数类型 + */ +type RetryCallback = () => void; + +/** + * Token刷新组合式函数 + */ +export function useTokenRefresh() { + // Token 刷新相关状态 + let isRefreshingToken = false; + const pendingRequests: RetryCallback[] = []; + + /** + * 刷新 Token 并重试请求 + */ + async function refreshTokenAndRetry( + config: InternalAxiosRequestConfig, + httpRequest: any + ): Promise { + return new Promise((resolve, reject) => { + // 封装需要重试的请求 + const retryRequest = () => { + const newToken = AuthStorage.getAccessToken(); + if (newToken && config.headers) { + config.headers.Authorization = `Bearer ${newToken}`; + } + httpRequest(config).then(resolve).catch(reject); + }; + + // 将请求加入等待队列 + pendingRequests.push(retryRequest); + + // 如果没有正在刷新,则开始刷新流程 + if (!isRefreshingToken) { + isRefreshingToken = true; + + useUserStoreHook() + .refreshToken() + .then(() => { + // 刷新成功,重试所有等待的请求 + pendingRequests.forEach((callback) => { + try { + callback(); + } catch (error) { + console.error("Retry request error:", error); + } + }); + // 清空队列 + pendingRequests.length = 0; + }) + .catch(async (error) => { + console.error("Token refresh failed:", error); + // 刷新失败,清空队列并跳转登录页 + pendingRequests.length = 0; + await redirectToLogin("登录状态已失效,请重新登录"); + // 拒绝所有等待的请求 + pendingRequests.forEach(() => { + reject(new Error("Token refresh failed")); + }); + }) + .finally(() => { + isRefreshingToken = false; + }); + } + }); + } + + /** + * 获取刷新状态(用于外部判断) + */ + function getRefreshStatus() { + return { + isRefreshing: isRefreshingToken, + pendingCount: pendingRequests.length, + }; + } + + return { + refreshTokenAndRetry, + getRefreshStatus, + }; +} diff --git a/src/composables/index.ts b/src/composables/index.ts index f9b4df02..f157c145 100644 --- a/src/composables/index.ts +++ b/src/composables/index.ts @@ -2,6 +2,7 @@ export { useStomp } from "./websocket/useStomp"; export { useDictSync } from "./websocket/useDictSync"; export type { DictMessage } from "./websocket/useDictSync"; export { useOnlineCount } from "./websocket/useOnlineCount"; +export { useTokenRefresh } from "./auth/useTokenRefresh"; export { useLayout } from "./layout/useLayout"; export { useLayoutMenu } from "./layout/useLayoutMenu"; diff --git a/src/settings.ts b/src/settings.ts index 9641874f..4ef966a1 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -34,6 +34,21 @@ export const defaultSettings: AppSettings = { sidebarColorScheme: SidebarColor.CLASSIC_BLUE, }; +/** + * 认证功能配置 + */ +export const authConfig = { + /** + * Token自动刷新开关 + * + * true: 启用自动刷新 - ACCESS_TOKEN_INVALID时尝试刷新token + * false: 禁用自动刷新 - ACCESS_TOKEN_INVALID时直接跳转登录页 + * + * 适用场景:后端没有刷新接口或不需要自动刷新的项目可设为false + */ + enableTokenRefresh: true, +} as const; + // 主题色预设 - 经典配色方案 // 注意:修改默认主题色时,需要同步修改 src/styles/variables.scss 中的 primary.base 值 export const themeColorPresets = [ diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 9b3193ff..e5931c0f 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,5 +1,7 @@ import { Storage } from "./storage"; import { AUTH_KEYS } from "@/constants"; +import { useUserStoreHook } from "@/store/modules/user-store"; +import router from "@/router"; // 负责本地凭证与偏好的读写 export const AuthStorage = { @@ -41,3 +43,25 @@ export const AuthStorage = { return Storage.get(AUTH_KEYS.REMEMBER_ME, false); }, }; + +/** + * 重定向到登录页面 + */ +export async function redirectToLogin(message: string = "请重新登录"): Promise { + try { + ElNotification({ + title: "提示", + message, + type: "warning", + duration: 3000, + }); + + await useUserStoreHook().resetAllState(); + + // 跳转到登录页,保留当前路由用于登录后跳转 + const currentPath = router.currentRoute.value.fullPath; + await router.push(`/login?redirect=${encodeURIComponent(currentPath)}`); + } catch (error) { + console.error("Redirect to login error:", error); + } +} diff --git a/src/utils/request.ts b/src/utils/request.ts index dda7f134..6da85474 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,9 +1,12 @@ import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from "axios"; import qs from "qs"; -import { useUserStoreHook } from "@/store/modules/user-store"; import { ApiCodeEnum } from "@/enums/api/code-enum"; -import { AuthStorage } from "@/utils/auth"; -import router from "@/router"; +import { AuthStorage, redirectToLogin } from "@/utils/auth"; +import { useTokenRefresh } from "@/composables/auth/useTokenRefresh"; +import { authConfig } from "@/settings"; + +// 初始化token刷新组合式函数 +const { refreshTokenAndRetry } = useTokenRefresh(); /** * 创建 HTTP 请求实例 @@ -42,8 +45,8 @@ httpRequest.interceptors.request.use( */ httpRequest.interceptors.response.use( (response: AxiosResponse) => { - // 如果响应是二进制流,则直接返回(用于文件下载、Excel 导出等) - if (response.config.responseType === "blob") { + // 如果响应是二进制数据,则直接返回response对象(用于文件下载、Excel导出、图片显示等) + if (response.config.responseType === "blob" || response.config.responseType === "arraybuffer") { return response; } @@ -73,8 +76,15 @@ httpRequest.interceptors.response.use( switch (code) { case ApiCodeEnum.ACCESS_TOKEN_INVALID: - // Access Token 过期,尝试刷新 - return refreshTokenAndRetry(config); + // Access Token 过期 + if (authConfig.enableTokenRefresh) { + // 启用了token刷新,尝试刷新 + return refreshTokenAndRetry(config, httpRequest); + } else { + // 未启用token刷新,直接跳转登录页 + await redirectToLogin("登录已过期,请重新登录"); + return Promise.reject(new Error(msg || "Access Token Invalid")); + } case ApiCodeEnum.REFRESH_TOKEN_INVALID: // Refresh Token 过期,跳转登录页 @@ -88,87 +98,4 @@ httpRequest.interceptors.response.use( } ); -/** - * 重试请求的回调函数类型 - */ -type RetryCallback = () => void; - -// Token 刷新相关状态 -let isRefreshingToken = false; -const pendingRequests: RetryCallback[] = []; - -/** - * 刷新 Token 并重试请求 - */ -async function refreshTokenAndRetry(config: InternalAxiosRequestConfig): Promise { - return new Promise((resolve, reject) => { - // 封装需要重试的请求 - const retryRequest = () => { - const newToken = AuthStorage.getAccessToken(); - if (newToken && config.headers) { - config.headers.Authorization = `Bearer ${newToken}`; - } - httpRequest(config).then(resolve).catch(reject); - }; - - // 将请求加入等待队列 - pendingRequests.push(retryRequest); - - // 如果没有正在刷新,则开始刷新流程 - if (!isRefreshingToken) { - isRefreshingToken = true; - - useUserStoreHook() - .refreshToken() - .then(() => { - // 刷新成功,重试所有等待的请求 - pendingRequests.forEach((callback) => { - try { - callback(); - } catch (error) { - console.error("Retry request error:", error); - } - }); - // 清空队列 - pendingRequests.length = 0; - }) - .catch(async (error) => { - console.error("Token refresh failed:", error); - // 刷新失败,清空队列并跳转登录页 - pendingRequests.length = 0; - await redirectToLogin("登录状态已失效,请重新登录"); - // 拒绝所有等待的请求 - pendingRequests.forEach(() => { - reject(new Error("Token refresh failed")); - }); - }) - .finally(() => { - isRefreshingToken = false; - }); - } - }); -} - -/** - * 重定向到登录页面 - */ -async function redirectToLogin(message: string = "请重新登录"): Promise { - try { - ElNotification({ - title: "提示", - message, - type: "warning", - duration: 3000, - }); - - await useUserStoreHook().resetAllState(); - - // 跳转到登录页,保留当前路由用于登录后跳转 - const currentPath = router.currentRoute.value.fullPath; - await router.push(`/login?redirect=${encodeURIComponent(currentPath)}`); - } catch (error) { - console.error("Redirect to login error:", error); - } -} - export default httpRequest; diff --git a/src/views/demo/auto-opreation-column.vue b/src/views/demo/auto-operation-column.vue similarity index 100% rename from src/views/demo/auto-opreation-column.vue rename to src/views/demo/auto-operation-column.vue diff --git a/src/views/demo/curd-demo.vue b/src/views/demo/curd-single.vue similarity index 100% rename from src/views/demo/curd-demo.vue rename to src/views/demo/curd-single.vue diff --git a/src/views/demo/icon-selector.vue b/src/views/demo/icon-select.vue similarity index 100% rename from src/views/demo/icon-selector.vue rename to src/views/demo/icon-select.vue