无感刷新token这个问题,一直来都是面试中常见的问题,相信有不少同学也被问到过,今天就一起来讨论下这个。
概述
无感刷新是一种优化用户体验的手段。它允许应用在用户首次登录之后,通过后端服务获取一组具有时效性的认证凭据,通常是一个Token和一个RefreshToken,其中Token作为用户身份的直接证明,而RefreshToken则用于在Token过期时静默更新认证状态,从而避免了用户因为认证凭据过期而频繁重新登录这种情况。
思路
首先我们要先捋清楚整个流程的思路
主要流程大概就是:
- 用户发出请求
- 后端验证token是否失效
- 没有失效的话,返回正常数据(结束)
- 失效的话,返回对应状态码
- 前端判断失效,调用刷新接口更新token
- 重新执行第一步
具体该怎么做
基于axios
axios
是目前普遍使用的基于Promise的http库,它有一个拦截器,可以在请求或者响应被then
或catch
处理前拦截它们。在这里,我们可以通过响应拦截拿到返回的报文,并在这里做出对Token失效的处理
举个栗子:
js
// request.js
import { updateToken } from '@/api/index.js' // 导入刷新token方法
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
const { status } = error.response.status // 获取到返回状态码
if (status === 401) { // 401状态码的意思是需要身份验证(表示token已经失效)
updateToken().then(res => { // 调用刷新token方法
const { token, refreshToken} = res.data // 拿到更新后的token和refreshToken
localstorage.setItem('token', {token, refreshToken}) // 更新本地存储
axios({token}) // 使用新的token重新发送请求
})
} else {
return Promise.reject(error);
}
});
基于class封装
对于大型项目,可能需要根据不同的状态码进行多种业务处理。这种情况下,将无感刷新Token的逻辑封装在一个独立的类中,可以避免逻辑耦合,并提高代码的可维护性。
举个栗子:
js
class RetryRequest {
constructor({
url,
getRefreshToken,
unauthorizedCode = 401,
onSuccess,
onError
}) {
this.url = url,
this.getRefreshToken = getRefreshToken
this.unauthorizedCode = unauthorizedCode,
this.onSuccess = onSuccess,
this.onError = onError
}
requestWrapper(request) {
return new Promise((resolve, reject) => {
const requestFn = request
return request()
.then(res => {
resolve(res)
})
.catch(err => {
// 表示token失效,需要更新token
if (err.response.status === this.unauthorizedCode) {
this.fetchNewToken({ // 更新token
headers: { Authorization: this.getRefreshToken() }
})
.then(requestFn()) // 成功则重新发起之前失败的请求
.catch(err => {})
}
})
})
}
// 获取新token的函数
fetchNewToken(config) {
return http.get(this.url, config)
.then(this.onSuccess())
.catch(this.onError())
}
}
js
// 使用
const getRefreshToken = () => {
const { REFRESH_KEY } = JSON.parse(localStorage.getItem('token'))
return REFRESH_KEY
}
const axiosRetry = new RetryRequest({
url: '/refreshToken',
unauthorizedCode: 401,
getRefreshToken,
onSuccess: () => {},
onError: () => {}
})
axiosRetry.requestWrapper(() => axios.get(url, options)) // 发送请求
以上的例子只可用作参考,不可照搬,具体写法需要根据实际业务需求进行调整
场景延伸
在实际应用中,可能会遇到多个请求同时触发Token刷新的情况。为了避免重复刷新Token和性能问题,我们可以引入一个控制机制,例如使用一个全局变量来标记当前是否正在刷新Token
以上面的axios为例子:
js
// request.js
let isRefreshing = false // 是否在刷新token
const updateToken = function() { // 重新请求token函数
isRefreshing = true // 打开开关
return axios.get('/refreshToken')
}
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
const { status } = error.response.status
if (status === 401 && !isRefreshing) { // token失效且开关为关闭状态才可以执行
updateToken().then(res => {
const { token, refreshToken} = res.data
localstorage.setItem('token', {token, refreshToken})
isRefreshing = false // 关闭开关
axios({token})
})
} else {
return Promise.reject(error);
}
});
上面这样就可以解决重复刷新token的问题了,但同时也会出现另一个问题:虽然不会重复刷新token,但因为逻辑没有往下走了,除了第一个请求外,后面那些请求不能自动重新请求了。针对这个问题,我们可以把思路稍微变一变:身份验证失败的请求我们可以存到一个数组里面,然后在更新token之后,再遍历数组执行里面的请求。
例:
js
// request.js
let isRefreshing = false // 是否在刷新token
const requestArr = [] // 请求缓存数组
const updateToken = function() { // 重新请求token函数
isRefreshing = true // 打开开关
return axios.get('/refreshToken')
}
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
const { status } = error.response.status
if (status === 401) {
requestArr.push(axios(config)) // 请求存进数组中
if (!isRefreshing) {
updateToken().then(res => {
const { token, refreshToken} = res.data
localstorage.setItem('token', {token, refreshToken})
requestArr.forEach(cb => cb()) // 遍历执行请求
}).finally(() => {
requestArr = [] // 清空数组
isRefreshing = false // 关闭开关
})
}
} else {
return Promise.reject(error);
}
});
至此,基本的一些使用思路和场景已经说完了。还是要根据具体的业务需求和场景来判断使用与否,以及进行调整和优化,以达到最佳的性能和用户体验。
如果有什么问题的话,可以在评论区留言,大家一起探讨学习,谢谢!