长短 token 介绍
平时后端返回的数据中表示 token 已过期时,前端如何处理?
常见解决方案:
- 重新登录以获取新的有效 token (可能打断正常的请求体验)
思路: 先将 store 中和本地保存的用户登录状态数据清除,然后跳转到登录页面
- 利用长短 token 来实现无感刷新以获取新的有效 token
长短 token 要配合服务端一下来实现:
通常服务端在用户登录时会生成两个 token,一个长 token(有效时间较长,如一个月),一个短 token(有效时间较短,如30分钟)。
前端在发送需要用户权限的网络请求时,携带短 token 进行认证,如果后端返回短 token 失效的数据,则前端重新发送请求根据长 token 到服务端换取有效的短 token,前端在获取到新的有效短 token 后会重新发送之前未正常处理的请求。
如果后端在处理前端根据长 token 换取有效短 token 时,发现长 token 也过期了,会向前端返回相应的数据。前端在获取到响应数据中发现是长 token 过期,则会清理前端用户登录状态数据,跳转到登录页面重新登录以 再次获取长短 token。
思路: 在 axios 二次封装时,响应拦截中进行无感刷新 token 拦截处理。
当响应返回的数据中表明短 token 过期时,再次利用 axios 发送刷新 token 的请求,将刷新 token 返回的数据进行处理(处理返回的有效 token 数据,比如保存到 store 中、保存到本地),重新发送前一次的网络请求:
如果有并发的多个网络请求都返回的是 token 过期,则需要进行相应优化:
利用一个 requests 数组来保存未正常完成的网络请求,利用一个 boolean 标记来标记是否正在刷新 token,结合 Promise 将未完成的网络请求保存到 requests 数组中,并让对应的 Promise 对象一直处于 pending 状态。
代码逻辑:
当其中最先返回的响应数据中表明 token 失效时(这时需要刷新 token),将刷新 token 的标记修改为 true(以阻止其它响应回来的数据会再次发送刷新 token 的请求),重新利用 axios 请求刷新 token 的接口;
其它后返回的响应数据中,发现 token 失效并已有正在刷新 token 的操作了,则将未正常完成的网络请示利用 Promise 保存到 requests 数组中;
当刷新 token 的请求在服务端处理完毕并响应后,将刷新 token 的标记修改为 false,然后将返回的新 token 保存到 store 或本地,继续重新未正常完成的网络请求,requests 数组中保存的其它未正常完成的请求也一并遍历并发送
import axios from 'axios'
// 保存所有未完成的网络请求
const requests = []
// 标记是否正在刷新新 token 中
let isRefreshing = false
// 创建实例
const service = axios.create({
baseURL:API,
timeout: 10000,
})
/**
* 请示拦截
*/
service.interceptors.request.use(
config => {
// 请示头中添加 token 的传递
const token = sessionStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error),
)
/**
* 响应拦截
*/
service.interceptors.response.use(
async response => {
// 响应状态码为 2xx,表示成功
if (response.status >= 200 && response.status < 300) {
// 获取后端响应数据
const { code, data, message } = response.data
// 判断后端响应数据的规范
if (code === 200) {
return data
}
// 如果返回的 code 表示的是用户 token 异常的问题
// 1. 通常前端会清除本地用户登录的状态信息,然后跳转到登录页面重新登录以获得正确的 token
// TODO...
// 2. 长短 token 的解决方案(无感刷新 token)
if (code === 4008) { // token 过期
if (!isRefreshing) {
// 标记刷新 token
isRefreshing = true
// token 过期后,重新请求服务端刷新 token 的 API 接口
// 以获取另一个在有效期内的新 token 进行使用
const { token } = await service
.get('API')
// 标记刷新 token 结束
isRefreshing = false
// 将获取到的新 token 保存到本地
sessionStorage.token = token
// 将之前未完成的网络请求重新发送完成
service(response.config)
requests.forEach(fn => fn())
} else {
return new Promise(resolve => {
requests.push(() => {
resolve(service(response.config))
})
})
}
}
// 其它问题,则进行全局错误提示
const error = new Error(message || '响应拦截时出现异常问题')
return Promise.reject(error)
}
return null
},
error => Promise.reject(error), // 出现异常,可进行全局错误提示
)
export default service