diff --git a/sheep/api/member/auth.js b/sheep/api/member/auth.js index 3cb98126..6ed6dfcb 100644 --- a/sheep/api/member/auth.js +++ b/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; diff --git a/sheep/request/index.js b/sheep/request/index.js index e1a1de80..ea3e25cc 100644 --- a/sheep/request/index.js +++ b/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) => { diff --git a/sheep/store/user.js b/sheep/store/user.js index d38715a0..d4a85f6f 100644 --- a/sheep/store/user.js +++ b/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;