From ad9dd5e1d1c56d3984faafba4668d87159c9f353 Mon Sep 17 00:00:00 2001 From: ray <1490493387@qq.com> Date: Thu, 14 Nov 2024 18:32:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20:sparkles:=20=E6=96=B0=E5=A2=9E=20JWT?= =?UTF-8?q?=20=E5=88=B7=E6=96=B0=E6=A8=A1=E5=BC=8F=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=9B=A0=E8=AE=BF=E9=97=AE=E4=BB=A4=E7=89=8C=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=E8=80=8C=E5=A4=B1=E8=B4=A5=E7=9A=84=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth/index.ts | 24 +++++----- src/enums/ResultEnum.ts | 9 +++- src/store/modules/user.ts | 28 +++++++++-- src/utils/auth.ts | 22 +++++++-- src/utils/request.ts | 98 ++++++++++++++++++++++++++------------- 5 files changed, 127 insertions(+), 54 deletions(-) diff --git a/src/api/auth/index.ts b/src/api/auth/index.ts index 299782b9..7728ad61 100644 --- a/src/api/auth/index.ts +++ b/src/api/auth/index.ts @@ -3,7 +3,7 @@ import request from "@/utils/request"; const AUTH_BASE_URL = "/api/v1/auth"; const AuthAPI = { - /** 登录 接口*/ + /** 登录接口*/ login(data: LoginData) { const formData = new FormData(); formData.append("username", data.username); @@ -20,7 +20,7 @@ const AuthAPI = { }); }, - /** 刷新token 接口*/ + /** 刷新 token 接口*/ refreshToken(refreshToken: string) { return request({ url: `${AUTH_BASE_URL}/refresh-token`, @@ -32,7 +32,7 @@ const AuthAPI = { }); }, - /** 注销 接口*/ + /** 注销接口*/ logout() { return request({ url: `${AUTH_BASE_URL}/logout`, @@ -40,7 +40,7 @@ const AuthAPI = { }); }, - /** 获取验证码 接口*/ + /** 获取验证码接口*/ getCaptcha() { return request({ url: `${AUTH_BASE_URL}/captcha`, @@ -65,14 +65,14 @@ export interface LoginData { /** 登录响应 */ export interface LoginResult { - /** 访问token */ - accessToken?: string; - /** 过期时间(单位:毫秒) */ - expires?: number; - /** 刷新token */ - refreshToken?: string; - /** token 类型 */ - tokenType?: string; + /** 访问令牌 */ + accessToken: string; + /** 刷新令牌 */ + refreshToken: string; + /** 令牌类型 */ + tokenType: string; + /** 过期时间(秒) */ + expiresIn: number; } /** 验证码响应 */ diff --git a/src/enums/ResultEnum.ts b/src/enums/ResultEnum.ts index 2dff7c9a..349a6289 100644 --- a/src/enums/ResultEnum.ts +++ b/src/enums/ResultEnum.ts @@ -12,7 +12,12 @@ export const enum ResultEnum { ERROR = "B0001", /** - * 令牌无效或过期 + * 访问令牌无效或过期 */ - TOKEN_INVALID = "A0230", + ACCESS_TOKEN_INVALID = "A0230", + + /** + * 刷新令牌无效或过期 + */ + REFRESH_TOKEN_INVALID = "A0231", } diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index b206dfa5..273e4fcb 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -5,7 +5,7 @@ import { useDictStoreHook } from "@/store/modules/dict"; import AuthAPI, { type LoginData } from "@/api/auth"; import UserAPI, { type UserInfo } from "@/api/system/user"; -import { setToken, clearToken } from "@/utils/auth"; +import { setToken, setRefreshToken, getRefreshToken, clearToken } from "@/utils/auth"; export const useUserStore = defineStore("user", () => { const userInfo = useStorage("userInfo", {} as UserInfo); @@ -20,8 +20,9 @@ export const useUserStore = defineStore("user", () => { return new Promise((resolve, reject) => { AuthAPI.login(loginData) .then((data) => { - const { tokenType, accessToken } = data; + const { tokenType, accessToken, refreshToken } = data; setToken(tokenType + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx + setRefreshToken(refreshToken); resolve(); }) .catch((error) => { @@ -69,7 +70,27 @@ export const useUserStore = defineStore("user", () => { } /** - * 清理用户会话 + * 刷新 token + */ + function refreshToken() { + const refreshToken = getRefreshToken(); + return new Promise((resolve, reject) => { + AuthAPI.refreshToken(refreshToken) + .then((data) => { + const { tokenType, accessToken, refreshToken } = data; + setToken(tokenType + " " + accessToken); + setRefreshToken(refreshToken); + resolve(); + }) + .catch((error) => { + console.log(" refreshToken 刷新失败", error); + reject(error); + }); + }); + } + + /** + * 清理用户数据 * * @returns */ @@ -88,6 +109,7 @@ export const useUserStore = defineStore("user", () => { login, logout, clearUserData, + refreshToken, }; }); diff --git a/src/utils/auth.ts b/src/utils/auth.ts index de588e96..bd3e9b3e 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,15 +1,27 @@ -const TOKEN_KEY = "admin-token"; +// 访问 token 缓存的 key +const ACCESS_TOKEN_KEY = "access_token"; +// 刷新 token 缓存的 key +const REFRESH_TOKEN_KEY = "refresh_token"; function getToken(): string { - return localStorage.getItem(TOKEN_KEY) || ""; + return localStorage.getItem(ACCESS_TOKEN_KEY) || ""; } function setToken(token: string) { - return localStorage.setItem(TOKEN_KEY, token); + localStorage.setItem(ACCESS_TOKEN_KEY, token); +} + +function getRefreshToken(): string { + return localStorage.getItem(REFRESH_TOKEN_KEY) || ""; +} + +function setRefreshToken(token: string) { + localStorage.setItem(REFRESH_TOKEN_KEY, token); } function clearToken() { - return localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(ACCESS_TOKEN_KEY); + localStorage.removeItem(REFRESH_TOKEN_KEY); } -export { getToken, setToken, clearToken }; +export { getToken, setToken, clearToken, getRefreshToken, setRefreshToken }; diff --git a/src/utils/request.ts b/src/utils/request.ts index 6ed449b5..36ef0754 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -3,43 +3,36 @@ import qs from "qs"; import { useUserStoreHook } from "@/store/modules/user"; import { ResultEnum } from "@/enums/ResultEnum"; import { getToken } from "@/utils/auth"; +import router from "@/router"; // 创建 axios 实例 const service = axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API, timeout: 50000, headers: { "Content-Type": "application/json;charset=utf-8" }, - paramsSerializer: (params) => { - return qs.stringify(params); - }, + paramsSerializer: (params) => qs.stringify(params), }); // 请求拦截器 service.interceptors.request.use( (config: InternalAxiosRequestConfig) => { - // 如果设置了 "no-auth",则不携带 Authorization 头,用于登录、刷新 token 等接口 - if (config.headers.Authorization === "no-auth") { - delete config.headers.Authorization; + const accessToken = getToken(); + // 如果 Authorization 设置为 no-auth,则不携带 Token,用于登录、刷新 Token 等接口 + if (config.headers.Authorization !== "no-auth" && accessToken) { + config.headers.Authorization = accessToken; } else { - const accessToken = getToken(); - if (accessToken) { - config.headers.Authorization = accessToken; - } + delete config.headers.Authorization; } return config; }, - (error: any) => { - return Promise.reject(error); - } + (error) => Promise.reject(error) ); // 响应拦截器 service.interceptors.response.use( (response: AxiosResponse) => { - const { responseType } = response.config; - - // 如果响应类型是二进制数据(文件导出场景), 则直接返回 response - if (responseType === "blob") { + // 如果响应是二进制流,则直接返回,用于下载文件、Excel 导出等 + if (response.config.responseType === "blob") { return response; } @@ -51,21 +44,15 @@ service.interceptors.response.use( ElMessage.error(msg || "系统出错"); return Promise.reject(new Error(msg || "Error")); }, - (error: any) => { - // 异常处理 非 2xx 状态码 会进入这里 - if (error.response.data) { - const { code, msg } = error.response.data; - if (code === ResultEnum.TOKEN_INVALID) { - ElNotification({ - title: "提示", - message: "您的会话已过期,请重新登录", - type: "info", - }); - useUserStoreHook() - .clearUserData() - .then(() => { - location.reload(); - }); + async (error: any) => { + const { config, response } = error; + if (response) { + const { code, msg } = response.data; + if (code === ResultEnum.ACCESS_TOKEN_INVALID) { + // Token 过期,刷新 Token + return handleTokenRefresh(config); + } else if (code === ResultEnum.REFRESH_TOKEN_INVALID) { + return Promise.reject(new Error(msg || "Error")); } else { ElMessage.error(msg || "系统出错"); } @@ -75,3 +62,50 @@ service.interceptors.response.use( ); export default service; + +// 刷新 Token 的锁 +let isRefreshing = false; +// 因 Token 过期导致失败的请求队列 +let requestsQueue: Array<() => void> = []; + +// 刷新 Token 处理 +async function handleTokenRefresh(config: InternalAxiosRequestConfig) { + return new Promise((resolve) => { + const requestCallback = () => { + config.headers.Authorization = getToken(); + resolve(service(config)); + }; + + requestsQueue.push(requestCallback); + + if (!isRefreshing) { + isRefreshing = true; + + // 刷新 Token + useUserStoreHook() + .refreshToken() + .then(() => { + // Token 刷新成功,执行请求队列 + requestsQueue.forEach((callback) => callback()); + requestsQueue = []; + }) + .catch((error) => { + console.log("handleTokenRefresh error", error); + // Token 刷新失败,清除用户数据并跳转到登录 + ElNotification({ + title: "提示", + message: "您的会话已过期,请重新登录", + type: "info", + }); + useUserStoreHook() + .clearUserData() + .then(() => { + router.push("/login"); + }); + }) + .finally(() => { + isRefreshing = false; + }); + } + }); +}