From d9e16baca991c0fefe01de1895dbc3f643b4e5bd Mon Sep 17 00:00:00 2001 From: Hygge <1727182921@qq.com> Date: Thu, 21 Aug 2025 15:49:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20token=20=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD=E5=8F=8A=E7=9B=B8=E5=85=B3=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=AE=9A=E4=B9=89=20-=20=E5=9C=A8=20typings.d.ts=20?= =?UTF-8?q?=E4=B8=AD=E6=96=B0=E5=A2=9E=20IUserToken=20=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=20-=20=E5=9C=A8=20login.ts=20=E4=B8=AD=E6=B7=BB=E5=8A=A0=20ref?= =?UTF-8?q?reshToken=20=E5=87=BD=E6=95=B0=E4=BB=A5=E6=94=AF=E6=8C=81=20tok?= =?UTF-8?q?en=20=E5=88=B7=E6=96=B0=20-=20=E5=9C=A8=20types/login.ts=20?= =?UTF-8?q?=E4=B8=AD=E6=96=B0=E5=A2=9E=20IUserTokenVo=20=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=20-=20=E6=9B=B4=E6=96=B0=20http.ts=20=E4=BB=A5=E5=A4=84?= =?UTF-8?q?=E7=90=86=20token=20=E5=88=B7=E6=96=B0=E9=80=BB=E8=BE=91=20-=20?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=20interceptor.ts=20=E4=BB=A5=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E6=96=B0=E7=9A=84=20token=20=E7=B1=BB=E5=9E=8B=20-=20?= =?UTF-8?q?=E5=9C=A8=20user.ts=20=E4=B8=AD=E6=95=B4=E5=90=88=20token=20?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- env/.env | 5 ++- src/api/login.ts | 8 +++++ src/api/types/login.ts | 10 ++++++ src/http/http.ts | 77 +++++++++++++++++++++++++++++++++++++---- src/http/interceptor.ts | 2 +- src/store/user.ts | 39 +++++++++++++++++++-- src/typings.d.ts | 7 +++- 7 files changed, 136 insertions(+), 12 deletions(-) diff --git a/env/.env b/env/.env index 8aa561c..f829255 100644 --- a/env/.env +++ b/env/.env @@ -21,4 +21,7 @@ VITE_APP_PROXY_ENABLE = true VITE_APP_PROXY_PREFIX = '/api' # 第二个请求地址 (目前alova中可以使用) -VITE_API_SECONDARY_URL = 'https://ukw0y1.laf.run' \ No newline at end of file +VITE_API_SECONDARY_URL = 'https://ukw0y1.laf.run' + +# TOKEN策略 +VITE_TOKEN_STRATEGY = 'single' \ No newline at end of file diff --git a/src/api/login.ts b/src/api/login.ts index de4266d..fcc72af 100644 --- a/src/api/login.ts +++ b/src/api/login.ts @@ -27,6 +27,14 @@ export function login(loginForm: ILoginForm) { return http.post('/user/login', loginForm) } +/** + * 刷新token + * @param refreshToken 刷新token + */ +export function refreshToken(refreshToken: string) { + return http.post('/user/refreshToken', { refreshToken }) +} + /** * 获取用户信息 */ diff --git a/src/api/types/login.ts b/src/api/types/login.ts index d0638cf..2a926b9 100644 --- a/src/api/types/login.ts +++ b/src/api/types/login.ts @@ -5,7 +5,15 @@ export interface IUserInfoVo { id: number username: string avatar: string +} + +/** + * 用户token + */ +export interface IUserTokenVo { token: string + refreshToken?: string + refreshExpire?: number } /** @@ -15,6 +23,8 @@ export interface IUserLogin { id: string username: string token: string + refreshToken?: string + refreshExpire?: number } /** diff --git a/src/http/http.ts b/src/http/http.ts index 98c7324..31bfaac 100644 --- a/src/http/http.ts +++ b/src/http/http.ts @@ -1,4 +1,13 @@ import type { CustomRequestOptions } from '@/http/types' +import { nextTick } from 'vue' +import { useUserStore } from '@/store/user' + +// 刷新 token 状态管理 +let refreshing = false // 防止重复刷新 token 标识 +let taskQueue: (() => void)[] = [] // 刷新 token 请求队列 + +// token 刷新策略: single-不刷新,double-无感刷新(需后端配合) +const sessionMode = import.meta.env.VITE_TOKEN_STRATEGY === 'double' ? 'double' : 'single' export function http(options: CustomRequestOptions) { // 1. 返回 Promise 对象 @@ -10,17 +19,71 @@ export function http(options: CustomRequestOptions) { responseType: 'json', // #endif // 响应成功 - success(res) { + success: async (res) => { // 状态码 2xx,参考 axios 的设计 if (res.statusCode >= 200 && res.statusCode < 300) { // 2.1 提取核心数据 res.data - resolve(res.data as IResData) + return resolve(res.data as IResData) } - else if (res.statusCode === 401) { - // 401错误 -> 清理用户信息,跳转到登录页 - // userStore.clearUserInfo() - // uni.navigateTo({ url: '/pages/login/login' }) - reject(res) + const resData: IResData = res.data as IResData + if ((res.statusCode === 401) || (resData.code === 401)) { + const store = useUserStore() + if (sessionMode === 'single') { + // 未启用双token策略,清理用户信息,跳转到登录页 + store.logout() + uni.navigateTo({ url: '/pages/login/login' }) + return reject(res) + } + /* -------- 无感刷新 token ----------- */ + const { refreshToken } = store.userToken || {} + // token 失效的,且有刷新 token 的,才放到请求队列里 + if ((res.statusCode === 401 || resData.code === 401) && refreshToken) { + taskQueue.push(() => { + resolve(http(options)) + }) + } + // 如果有 refreshToken 且未在刷新中,发起刷新 token 请求 + if ((res.statusCode === 401 || resData.code === 401) && refreshToken && !refreshing) { + refreshing = true + try { + // 发起刷新 token 请求(使用 store 的 refreshToken 方法) + await store.refreshToken() + // 刷新 token 成功 + refreshing = false + nextTick(() => { + // 关闭其他弹窗 + uni.hideToast() + uni.showToast({ + title: 'token 刷新成功', + icon: 'none', + }) + }) + // 将任务队列的所有任务重新请求 + taskQueue.forEach(task => task()) + } + catch (refreshErr) { + refreshing = false + // 刷新 token 失败,跳转到登录页 + nextTick(() => { + // 关闭其他弹窗 + uni.hideToast() + uni.showToast({ + title: '登录已过期,请重新登录', + icon: 'none', + }) + }) + // 清除用户信息 + await store.logout() + // 跳转到登录页 + setTimeout(() => { + uni.navigateTo({ url: '/pages/login/login' }) + }, 2000) + } + finally { + // 不管刷新 token 成功与否,都清空任务队列 + taskQueue = [] + } + } } else { // 其他错误 -> 根据后端错误信息轻提示 diff --git a/src/http/interceptor.ts b/src/http/interceptor.ts index 30350b4..7fb0d63 100644 --- a/src/http/interceptor.ts +++ b/src/http/interceptor.ts @@ -48,7 +48,7 @@ const httpInterceptor = { } // 3. 添加 token 请求头标识 const userStore = useUserStore() - const { token } = userStore.userInfo as unknown as IUserInfo + const { token } = userStore.userToken as unknown as IUserToken if (token) { options.header.Authorization = `Bearer ${token}` } diff --git a/src/store/user.ts b/src/store/user.ts index cec931a..4e189fa 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -1,10 +1,11 @@ -import type { IUserInfoVo } from '@/api/types/login' +import type { IUserInfoVo, IUserLogin, IUserTokenVo } from '@/api/types/login' import { defineStore } from 'pinia' import { ref } from 'vue' import { getUserInfo as _getUserInfo, login as _login, logout as _logout, + refreshToken as _refreshToken, wxLogin as _wxLogin, getWxCode, } from '@/api/login' @@ -15,7 +16,12 @@ const userInfoState: IUserInfoVo = { id: 0, username: '', avatar: '/static/images/default-avatar.png', +} + +const userTokenState: IUserTokenVo = { token: '', + refreshToken: '', + refreshExpire: 0, } export const useUserStore = defineStore( @@ -23,6 +29,8 @@ export const useUserStore = defineStore( () => { // 定义用户信息 const userInfo = ref({ ...userInfoState }) + const userToken = ref({ ...userTokenState }) + // 设置用户信息 const setUserInfo = (val: IUserInfoVo) => { console.log('设置用户信息', val) @@ -43,9 +51,23 @@ export const useUserStore = defineStore( // 删除用户信息 const removeUserInfo = () => { userInfo.value = { ...userInfoState } + userToken.value = { ...userTokenState } uni.removeStorageSync('userInfo') uni.removeStorageSync('token') + uni.removeStorageSync('refreshToken') } + + /** + * 存储token,非导出 + */ + const setToken = (tokenBody: IUserLogin) => { + userToken.value.token = tokenBody.token + userToken.value.refreshToken = tokenBody.refreshToken + userToken.value.refreshExpire = tokenBody.refreshExpire + uni.setStorageSync('token', tokenBody.token) + uni.setStorageSync('refreshToken', tokenBody.refreshToken) + } + /** * 获取用户信息 */ @@ -54,7 +76,6 @@ export const useUserStore = defineStore( const userInfo = res.data setUserInfo(userInfo) uni.setStorageSync('userInfo', userInfo) - uni.setStorageSync('token', userInfo.token) // TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由 return res } @@ -72,6 +93,18 @@ export const useUserStore = defineStore( const res = await _login(credentials) console.log('登录信息', res) toast.success('登录成功') + // 这里设置token 和 refreshToken + setToken(res.data) + await getUserInfo() + return res + } + + /** + * 刷新token + */ + const refreshToken = async () => { + const res = await _refreshToken(userToken.value.refreshToken) + setToken(res.data) await getUserInfo() return res } @@ -98,11 +131,13 @@ export const useUserStore = defineStore( return { userInfo, + userToken, login, wxLogin, getUserInfo, setUserAvatar, logout, + refreshToken, } }, { diff --git a/src/typings.d.ts b/src/typings.d.ts index 9ead3fb..99d7e65 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -21,7 +21,12 @@ declare global { avatar?: string /** 微信的 openid,非微信没有这个字段 */ openid?: string - token?: string + } + + interface IUserToken { + token: string + refreshToken?: string + refreshExpire?: number } }