Просмотр исходного кода

feat: 添加 token 刷新功能及相关类型定义

- 在 typings.d.ts 中新增 IUserToken 接口
- 在 login.ts 中添加 refreshToken 函数以支持 token 刷新
- 在 types/login.ts 中新增 IUserTokenVo 接口
- 更新 http.ts 以处理 token 刷新逻辑
- 修改 interceptor.ts 以使用新的 token 类型
- 在 user.ts 中整合 token 状态管理
Hygge 8 месяцев назад
Родитель
Сommit
d3c6823a48
6 измененных файлов с 131 добавлено и 6 удалено
  1. 8 0
      src/api/login.ts
  2. 10 0
      src/api/types/login.ts
  3. 70 2
      src/http/http.ts
  4. 1 1
      src/http/interceptor.ts
  5. 36 2
      src/store/user.ts
  6. 6 1
      src/typings.d.ts

+ 8 - 0
src/api/login.ts

@@ -27,6 +27,14 @@ export function login(loginForm: ILoginForm) {
   return http.post<IUserLogin>('/user/login', loginForm)
   return http.post<IUserLogin>('/user/login', loginForm)
 }
 }
 
 
+/**
+ * 刷新token
+ * @param refreshToken 刷新token
+ */
+export function refreshToken(refreshToken: string) {
+  return http.post<IUserLogin>('/user/refreshToken', { refreshToken })
+}
+
 /**
 /**
  * 获取用户信息
  * 获取用户信息
  */
  */

+ 10 - 0
src/api/types/login.ts

@@ -5,7 +5,15 @@ export interface IUserInfoVo {
   id: number
   id: number
   username: string
   username: string
   avatar: string
   avatar: string
+}
+
+/**
+ * 用户token
+ */
+export interface IUserTokenVo {
   token: string
   token: string
+  refreshToken?: string
+  refreshExpire?: number
 }
 }
 
 
 /**
 /**
@@ -15,6 +23,8 @@ export interface IUserLogin {
   id: string
   id: string
   username: string
   username: string
   token: string
   token: string
+  refreshToken?: string
+  refreshExpire?: number
 }
 }
 
 
 /**
 /**

+ 70 - 2
src/http/http.ts

@@ -1,4 +1,10 @@
 import type { CustomRequestOptions } from '@/http/types'
 import type { CustomRequestOptions } from '@/http/types'
+import { nextTick } from 'vue'
+import { useUserStore } from '@/store/user'
+
+// 刷新 token 状态管理
+let refreshing = false // 防止重复刷新 token 标识
+let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
 
 
 export function http<T>(options: CustomRequestOptions) {
 export function http<T>(options: CustomRequestOptions) {
   // 1. 返回 Promise 对象
   // 1. 返回 Promise 对象
@@ -10,18 +16,80 @@ export function http<T>(options: CustomRequestOptions) {
       responseType: 'json',
       responseType: 'json',
       // #endif
       // #endif
       // 响应成功
       // 响应成功
-      success(res) {
+      success: async (res) => {
         // 状态码 2xx,参考 axios 的设计
         // 状态码 2xx,参考 axios 的设计
         if (res.statusCode >= 200 && res.statusCode < 300) {
         if (res.statusCode >= 200 && res.statusCode < 300) {
           // 2.1 提取核心数据 res.data
           // 2.1 提取核心数据 res.data
-          resolve(res.data as IResData<T>)
+          return resolve(res.data as IResData<T>)
         }
         }
+        // 原策略1: 清理用户信息,跳转到登录页
         else if (res.statusCode === 401) {
         else if (res.statusCode === 401) {
           // 401错误  -> 清理用户信息,跳转到登录页
           // 401错误  -> 清理用户信息,跳转到登录页
           // userStore.clearUserInfo()
           // userStore.clearUserInfo()
           // uni.navigateTo({ url: '/pages/login/login' })
           // uni.navigateTo({ url: '/pages/login/login' })
           reject(res)
           reject(res)
         }
         }
+
+        /* -------- 策略2:无感刷新 token ----------- */
+        const store = useUserStore()
+        const { refreshToken } = store.userToken || {}
+
+        // token 失效的,且有刷新 token 的,才放到请求队列里
+        const resData: IResData<T> = res.data as IResData<T>
+        if ((res.statusCode === 401 || resData.code === 401) && refreshToken) {
+          taskQueue.push(() => {
+            resolve(http<T>(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 {
         else {
           // 其他错误 -> 根据后端错误信息轻提示
           // 其他错误 -> 根据后端错误信息轻提示
           !options.hideErrorToast
           !options.hideErrorToast

+ 1 - 1
src/http/interceptor.ts

@@ -48,7 +48,7 @@ const httpInterceptor = {
     }
     }
     // 3. 添加 token 请求头标识
     // 3. 添加 token 请求头标识
     const userStore = useUserStore()
     const userStore = useUserStore()
-    const { token } = userStore.userInfo as unknown as IUserInfo
+    const { token } = userStore.userToken as unknown as IUserToken
     if (token) {
     if (token) {
       options.header.Authorization = `Bearer ${token}`
       options.header.Authorization = `Bearer ${token}`
     }
     }

+ 36 - 2
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 { defineStore } from 'pinia'
 import { ref } from 'vue'
 import { ref } from 'vue'
 import {
 import {
   getUserInfo as _getUserInfo,
   getUserInfo as _getUserInfo,
   login as _login,
   login as _login,
   logout as _logout,
   logout as _logout,
+  refreshToken as _refreshToken,
   wxLogin as _wxLogin,
   wxLogin as _wxLogin,
   getWxCode,
   getWxCode,
 } from '@/api/login'
 } from '@/api/login'
@@ -15,7 +16,12 @@ const userInfoState: IUserInfoVo = {
   id: 0,
   id: 0,
   username: '',
   username: '',
   avatar: '/static/images/default-avatar.png',
   avatar: '/static/images/default-avatar.png',
+}
+
+const userTokenState: IUserTokenVo = {
   token: '',
   token: '',
+  refreshToken: '',
+  refreshExpire: 0,
 }
 }
 
 
 export const useUserStore = defineStore(
 export const useUserStore = defineStore(
@@ -23,6 +29,7 @@ export const useUserStore = defineStore(
   () => {
   () => {
     // 定义用户信息
     // 定义用户信息
     const userInfo = ref<IUserInfoVo>({ ...userInfoState })
     const userInfo = ref<IUserInfoVo>({ ...userInfoState })
+    const userToken = ref<IUserTokenVo>({ ...userTokenState })
     // 设置用户信息
     // 设置用户信息
     const setUserInfo = (val: IUserInfoVo) => {
     const setUserInfo = (val: IUserInfoVo) => {
       console.log('设置用户信息', val)
       console.log('设置用户信息', val)
@@ -43,9 +50,23 @@ export const useUserStore = defineStore(
     // 删除用户信息
     // 删除用户信息
     const removeUserInfo = () => {
     const removeUserInfo = () => {
       userInfo.value = { ...userInfoState }
       userInfo.value = { ...userInfoState }
+      userToken.value = { ...userTokenState }
       uni.removeStorageSync('userInfo')
       uni.removeStorageSync('userInfo')
       uni.removeStorageSync('token')
       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 +75,6 @@ export const useUserStore = defineStore(
       const userInfo = res.data
       const userInfo = res.data
       setUserInfo(userInfo)
       setUserInfo(userInfo)
       uni.setStorageSync('userInfo', userInfo)
       uni.setStorageSync('userInfo', userInfo)
-      uni.setStorageSync('token', userInfo.token)
       // TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由
       // TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由
       return res
       return res
     }
     }
@@ -72,6 +92,18 @@ export const useUserStore = defineStore(
       const res = await _login(credentials)
       const res = await _login(credentials)
       console.log('登录信息', res)
       console.log('登录信息', res)
       toast.success('登录成功')
       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()
       await getUserInfo()
       return res
       return res
     }
     }
@@ -98,11 +130,13 @@ export const useUserStore = defineStore(
 
 
     return {
     return {
       userInfo,
       userInfo,
+      userToken,
       login,
       login,
       wxLogin,
       wxLogin,
       getUserInfo,
       getUserInfo,
       setUserAvatar,
       setUserAvatar,
       logout,
       logout,
+      refreshToken,
     }
     }
   },
   },
   {
   {

+ 6 - 1
src/typings.d.ts

@@ -21,7 +21,12 @@ declare global {
     avatar?: string
     avatar?: string
     /** 微信的 openid,非微信没有这个字段 */
     /** 微信的 openid,非微信没有这个字段 */
     openid?: string
     openid?: string
-    token?: string
+  }
+
+  interface IUserToken {
+    token: string
+    refreshToken?: string
+    refreshExpire?: number
   }
   }
 }
 }