瀏覽代碼

✨ request:增加访问令牌的刷新机制!

YunaiV 1 年之前
父節點
當前提交
80f80e358f
共有 3 個文件被更改,包括 98 次插入13 次删除
  1. 15 0
      sheep/api/member/auth.js
  2. 80 11
      sheep/request/index.js
  3. 3 2
      sheep/store/user.js

+ 15 - 0
sheep/api/member/auth.js

@@ -50,6 +50,20 @@ const AuthUtil = {
       method: 'POST',
     });
   },
+  // 刷新令牌
+  refreshToken: (refreshToken) => {
+    return request({
+      url: '/app-api/member/auth/refresh-token',
+      method: 'POST',
+      params: {
+        refreshToken
+      },
+      custom: {
+        loading: false, // 不用加载中
+        showError: false, // 不展示错误提示
+      },
+    });
+  },
   // 社交授权的跳转
   socialAuthRedirect: (type, redirectUri) => {
     return request({
@@ -112,6 +126,7 @@ const AuthUtil = {
       },
     })
   },
+  //
 };
 
 export default AuthUtil;

+ 80 - 11
sheep/request/index.js

@@ -13,6 +13,7 @@ import $platform from '@/sheep/platform';
 import {
 	showAuthModal
 } from '@/sheep/hooks/useModal';
+import AuthUtil from '@/sheep/api/member/auth';
 
 const options = {
 	// 显示操作成功消息 默认不显示
@@ -93,8 +94,10 @@ http.interceptors.request.use(
 		}
 
     // 增加 token 令牌、terminal 终端、tenant 租户的请求头
-		const token = uni.getStorageSync('token');
-		if (token) config.header['Authorization'] = token;
+		const token = getAccessToken();
+		if (token) {
+      config.header['Authorization'] = token;
+    }
 		// TODO 芋艿:特殊处理
 		if (config.url.indexOf('/app-api/') !== -1) {
 			config.header['Accept'] = '*/*'
@@ -116,7 +119,7 @@ http.interceptors.response.use(
 	(response) => {
 		// 约定:如果是 /auth/ 下的 URL 地址,并且返回了 accessToken 说明是登录相关的接口,则自动设置登陆令牌
 		if (response.config.url.indexOf('/member/auth/') >= 0 && response.data?.data?.accessToken) {
-			$store('user').setToken(response.data.data.accessToken);
+			$store('user').setToken(response.data.data.accessToken, response.data.data.refreshToken);
 		}
 
     // 自定处理【loading 加载中】:如果需要显示 loading,则关闭 loading
@@ -126,7 +129,7 @@ http.interceptors.response.use(
 		if (response.data.code !== 0) {
       // 特殊:如果 401 错误码,则跳转到登录页 or 刷新令牌
       if (response.data.code === 401) {
-        handleAuthorized();
+        return refreshToken(response.config);
       }
 
       // 错误提示
@@ -162,13 +165,9 @@ http.interceptors.response.use(
 					errorMessage = '请求错误';
 					break;
 				case 401:
-					if (isLogin) {
-						errorMessage = '您的登陆已过期';
-					} else {
-						errorMessage = '请先登录';
-					}
-          handleAuthorized()
-					break;
+          errorMessage = isLogin ? '您的登陆已过期' : '请先登录';
+          // 正常情况下,后端不会返回 401 错误,所以这里不处理 handleAuthorized
+          break;
 				case 403:
 					errorMessage = '拒绝访问';
 					break;
@@ -222,6 +221,61 @@ http.interceptors.response.use(
 	},
 );
 
+// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现
+let requestList = [] // 请求队列
+let isRefreshToken = false // 是否正在刷新中
+const refreshToken = async (config) => {
+  // 如果当前已经是 refresh-token 的 URL 地址,并且还是 401 错误,说明是刷新令牌失败了,直接返回 Promise.reject(error)
+  if (config.url.indexOf('/member/auth/refresh-token') >= 0) {
+    return Promise.reject('error')
+  }
+
+  // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
+  if (!isRefreshToken) {
+    isRefreshToken = true
+    // 1. 如果获取不到刷新令牌,则只能执行登出操作
+    const refreshToken = getRefreshToken()
+    if (!refreshToken) {
+      return handleAuthorized()
+    }
+    // 2. 进行刷新访问令牌
+    try {
+      const refreshTokenResult = await AuthUtil.refreshToken(refreshToken);
+      if (refreshTokenResult.code !== 0) {
+        // 如果刷新不成功,直接抛出 e 触发 2.2 的逻辑
+        // noinspection ExceptionCaughtLocallyJS
+        throw new Error('刷新令牌失败');
+      }
+      // 2.1 刷新成功,则回放队列的请求 + 当前请求
+      config.header.Authorization = 'Bearer ' + getAccessToken()
+      requestList.forEach((cb) => {
+        cb()
+      })
+      requestList = []
+      return request(config)
+    } catch (e) {
+      // 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
+      // 2.2 刷新失败,只回放队列的请求
+      requestList.forEach((cb) => {
+        cb()
+      })
+      // 提示是否要登出。即不回放当前请求!不然会形成递归
+      return handleAuthorized()
+    } finally {
+      requestList = []
+      isRefreshToken = false
+    }
+  } else {
+    // 添加到队列,等待刷新获取到新的令牌
+    return new Promise((resolve) => {
+      requestList.push(() => {
+        config.header.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
+        resolve(request(config))
+      })
+    })
+  }
+}
+
 /**
  * 处理 401 未登录的错误
  */
@@ -229,6 +283,21 @@ const handleAuthorized = () => {
   const userStore = $store('user');
   userStore.logout(true);
   showAuthModal();
+  // 登录超时
+  return Promise.reject({
+    code: 401,
+    msg: userStore.isLogin ? '您的登陆已过期' : '请先登录'
+  })
+}
+
+/** 获得访问令牌 */
+const getAccessToken = () => {
+  return uni.getStorageSync('token');
+}
+
+/** 获得刷新令牌 */
+const getRefreshToken = () => {
+  return uni.getStorageSync('refresh-token');
 }
 
 const request = (config) => {

+ 3 - 2
sheep/store/user.js

@@ -99,14 +99,15 @@ const user = defineStore({
 		},
 
 		// 设置 token
-    // TODO 芋艿:后续要支持访问令牌的刷新!!!
-    setToken(token = '') {
+    setToken(token = '', refreshToken = '') {
 			if (token === '') {
 				this.isLogin = false;
 				uni.removeStorageSync('token');
+        uni.removeStorageSync('refresh-token')
 			} else {
 				this.isLogin = true;
 				uni.setStorageSync('token', token);
+        uni.setStorageSync('refresh-token', refreshToken);
 				this.loginAfter();
 			}
 			return this.isLogin;