引言
在现代Web和移动应用中,Token是用于身份认证的重要手段,它帮助服务器识别用户身份并提供相应的权限,然而,Token通常会有有效期,过期后用户将无法继续访问需要身份验证的接口,Token过期的问题,如果处理不当,会严重影响用户体验,特别是在UniApp这种跨平台开发框架中,我们需要确保不同平台的用户在Token过期时能平稳过渡
1. 什么是Token过期?
Token通常是一个短期有效的字符串,存储在客户端,后端服务器通过验证Token来识别用户身份,然而Token一般会设定一个过期时间,一旦超过这个时间,Token就会失效,常见的错误码如401 Unauthorized 标识Token过期,需要重新登录或刷新Token
2.Token过期常见问题
- 用户体验差: Token一旦过期,用户可能会遇到"请重新登录"的提示,影响流畅性
- 接口调用失败: 如果前端没有妥善处理Token过期,接口调用会缺少有效Token而失败
- 刷新失败: 当尝试刷新Token失败时,用户需要重新登录,这时候的体验可能会比较差
那么,如何在UniApp中高效处理Token过期问题呢? 下面我将介绍几种常见的解决方案
3. 解决方案
方案1: 基础版 - 被动刷新(Token 过期后提示 + 手动登录)
核心逻辑:
拦截接口返回的 401 (Token过期),清除本地 Token 并跳转登录页,适合小型项目,无refreshToken 机制。
方案2: 无感刷新 双 Token 机制 (自动续期 + 请求队列)
核心逻辑: 当Access Token 失效(后端返回401状态码)时
双Token无感刷新
如果只给用户一个Token:
- 设短了 (比如2个小时) : 用户用着用着就过期,需要重新登录,体验差
- 设长了 (比如7天) : 一旦Token 泄露,别人能长期盗用,不安全
- Access Token(访问令牌) : 短有效期(2小时),请求接口
- Refresh Token(刷新令牌) 仅用于Access Token 过期后的续期操作,有效期长(如604800秒),不参与普通接口请求,
技术架构
1. Token Store 状态管理
使用Pinia 管理Token状态,核心功能包括
1.1 过期时间管理
登录成功后,计算并存储Token 的过期时间戳:
javascript
const setTokenInfo = (val: IAuthLoginRes) => {
updateNowTime()
tokenInfo.value = val
// 计算并存储过期时间
const now = Date.now()
if (isSingleTokenRes(val)) {
// 单token模式
const expireTime = now + val.expiresIn * 1000
uni.setStorageSync('accessTokenExpireTime', expireTime)
}
else if (isDoubleTokenRes(val)) {
// 双token模式
const accessExpireTime = now + val.accessExpiresIn * 1000
const refreshExpireTime = now + val.refreshExpiresIn * 1000
uni.setStorageSync('accessTokenExpireTime', accessExpireTime)
uni.setStorageSync('refreshTokenExpireTime', refreshExpireTime)
}
}
2.2 过期状态判断
通过computed 属性实时判断Token 是否过期:
javascript
//令牌
const isTokenExpired = computed(() => {
if (!tokenInfo.value) {
return true
}
const now = nowTime.value
const expireTime = uni.getStorageSync('accessTokenExpireTime')
if (!expireTime)
return true
return now >= expireTime
})
//刷新令牌
const isRefreshTokenExpired = computed(() => {
if (!isDoubleTokenMode)
return true
const now = nowTime.value
const refreshExpireTime = uni.getStorageSync('refreshTokenExpireTime')
if (!refreshExpireTime)
return true
return now >= refreshExpireTime
})
```
2.3 刷新token 方法
javascript
``typescript
const refreshToken = async () => {
if (!isDoubleTokenMode) {
console.error('单token模式不支持刷新token')
throw new Error('单token模式不支持刷新token')
}
try {
// 安全检查,确保refreshToken存在
if (!isDoubleTokenRes(tokenInfo.value) || !tokenInfo.value.refreshToken) {
throw new Error('无效的refreshToken')
}
const refreshToken = tokenInfo.value.refreshToken
const res = await _refreshToken(refreshToken)
console.log('刷新token-res: ', res)
setTokenInfo(res)
return res
}
catch (error) {
console.error('刷新token失败:', error)
throw error
}
finally {
updateNowTime()
}
}
2.4. HTTP 拦截器 自动添加token
在请求拦截器中,自动为每个请求添加有效的 Token:
javascript
const httpInterceptor = {
invoke(options: CustomRequestOptions) {
// ... 其他处理逻辑
// 添加 token 请求头标识
const tokenStore = useTokenStore()
const token = tokenStore.updateNowTime().validToken
if (token) {
options.header.Authorization = `Bearer ${token}`
}
return options
},
}
2.5 无感刷新核心逻辑 (401错误)
这是整个方案的核心, 在HTTP 响应拦截中实现:
步骤1: 获取Refresh Token 并加入请求队列
从Pinia的Token 仓库中取出refreshToken :
- 如有refreshToken : 将当前失败的请求封装回调函数,加入taskQueue 队列(回调函数逻辑:用新Token 重新发起原请求,并resolve 结果)
- 若没有refreshToken: 直接跳过刷新,后续会走失败逻辑
步骤2: 加锁并发起Token刷新
判断条件:refreshToken && !refreshing(有刷新令牌且未在刷新中):
先设置 refreshing = true(加锁),防止其他请求重复触发刷新;
调用 tokenStore.refreshToken() 发起刷新请求:
- 刷新成功:
- 解锁(
refreshing = false); - 提示「token 刷新成功」(可注释,真正无感);
- 遍历
taskQueue执行所有回调函数(重新发起之前失败的请求); - 最后清空队列(
taskQueue = [])。
- 解锁(
- 刷新失败(如 Refresh Token 过期):
- 解锁(
refreshing = false); - 提示「登录已过期,请重新登录」;
- 清空本地登录态(
tokenStore.logout()); - 延迟 2 秒跳转登录页;
- 清空队列(
taskQueue = [])。
- 解锁(
步骤3: 临时 reject 原请求
无论是否触发刷新,当前 401 对应的原请求会先 return reject(res) ------ 因为原请求用的是过期 Token,
4. 正常响应处理
若响应不是 401 错误:
- 判断 HTTP 状态码是否在 200-300 之间(合法响应);
- 再判断业务码是否为成功码(
ResultEnum.Success0/Success200):- 成功:resolve 后端返回的业务数据;
- 失败:提示错误信息,reject 错误数据;
- 非 200-300 状态码:直接 reject 响应结果。
必然失败,后续靠队列重新执行请求并 resolve 结果。
javascript
```typescript
// 刷新 token 状态管理
let refreshing = false // 防止重复刷新 token 标识
let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
export function http<T>(options: CustomRequestOptions) {
return new Promise<T>((resolve, reject) => {
uni.request({
...options,
success: async (res) => {
const responseData = res.data as IResponse<T>
const { code } = responseData
// 检查是否是401错误
const isTokenExpired = res.statusCode === 401 || code === 401
if (isTokenExpired) {
const tokenStore = useTokenStore()
// 单Token模式直接跳转登录
if (!isDoubleTokenMode) {
tokenStore.logout()
toLoginPage()
return reject(res)
}
/* -------- 无感刷新 token ----------- */
const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
// 将失败的请求加入队列
if (refreshToken) {
taskQueue.push(() => {
resolve(http<T>(options))
})
}
// 如果有 refreshToken 且未在刷新中,发起刷新
if (refreshToken && !refreshing) {
refreshing = true
try {
// 刷新 token
await tokenStore.refreshToken()
refreshing = false
uni.showToast({
title: 'token 刷新成功',
icon: 'none',
})
// 重新执行队列中的所有请求
taskQueue.forEach(task => task())
}
catch (refreshErr) {
console.error('刷新 token 失败:', refreshErr)
refreshing = false
uni.showToast({
title: '登录已过期,请重新登录',
icon: 'none',
})
// 清除用户信息并跳转登录页
await tokenStore.logout()
setTimeout(() => {
toLoginPage()
}, 2000)
}
finally {
// 清空任务队列
taskQueue = []
}
}
return reject(res)
}
// 处理正常响应
if (res.statusCode >= 200 && res.statusCode < 300) {
if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
uni.showToast({
icon: 'none',
title: responseData.msg || responseData.message || '请求错误',
})
return reject(responseData.data)
}
return resolve(responseData.data)
}
// 处理其他错误
reject(res)
},
fail(err) {
uni.showToast({
icon: 'none',
title: '网络错误,换个网络试试',
})
reject(err)
},
})
})
}
工作流程详解
场景一: 单个请求Token 过期
用户操作 -> 发起请求 -> 携带过期Token
服务器返回401
检测到401 -> 加入请求队列 -> 发起刷新Token 请求
刷新成功-> 更新token -> 重新执行队列中的请求
返回数据给用户(用户无感知)
场景二:多个请求同时 Token 过期
请求A、B、C 同时发起 → 都携带过期Token
都返回 401
请求A:触发刷新,设置 refreshing = true
请求B:加入队列等待
请求C:加入队列等待
刷新成功 → 更新Token
队列中的请求A、B、C 依次重新执行
所有请求成功返回(用户无感知)
### 场景三:Refresh Token 也过期
用户操作 → 发起请求 → 携带过期Token
服务器返回 401
尝试刷新Token → Refresh Token 也过期
刷新失败 → 清除本地Token → 提示用户
2秒后跳转登录页
方案3 : 前端主动预刷新
核心逻辑
- 不在401 之后再刷新
- 在 Access Token 快要过期前(比如快过期前1分钟)
- 前端主动静默刷新Token
流程
- 登录时记录过期时间
- 定时器 / 每次发请求前判断: 是否快过期
- 是 -> 提前刷新Token
- 否 -> 正常请求
方案 4 : 单Token + 后端延长有效期
核心逻辑
- 只有一个 Access Token
- 每次请求接口,后端自动延长 Token 有效期
- 前端不用管刷新
方案 5:每次请求都带 Token,后端实时判断(懒人方案)
- 前端不判断过期
- 每次请求都带上 Token\
- 后端发现快过期,直接在响应头返回新 Token
- 前端拦截响应,自动替换旧 Token