1243 字
6 分钟
双token认证
场景
TIP 在开发中为了安全或满足分布式场景,我们有时会选择使用
JWT(Json Web Token)
的认证手段。但是使用token难免遇到有效期的问题,如果token长期有效,服务端不断发布新的token,导致有效的token越来越多,一旦token泄露,存在很大的安全隐患。而如果我们缩短token的有效期,为了用户体验性,就要做到无感刷新
方案
1. 使用旧的Token获取新Token
WARNING 如果采取单个token要实现token的自动刷新,就得使用定时器,每隔一段时间自动刷新token,并且这个token需要是没有过期的,因为如果已经过期的token也可以用来刷新,那token就和永久有效一样了
但这种方案存在一些问题:
- 存在并发请求时,可能前一个请求携带的还是旧token,此时又到了刷新token的时间,就会造成请求的token和服务端存储的token不一致的情况
- 使用定时器同时增加了性能损耗
2. 使用双Token的方式进行无感刷新
TIP 这种方案的大致流程为: 登录后客户端收到两个token(access_token,refresh_token),其中access_token用来身份认证,refresh_token用来刷新access_token
这种方案的其中一个好处是:access_token的有效期很短(比如2小时),refresh_token的有效期较长(比如7天)。access_token在需要权限的请求中都需要携带,使用比较频繁,泄露的风险较大,这种方式下即使access_token泄露,也很快就过期了。而refresh_token只有在刷新access_token的时候需要使用,暴露的风险比较小,一定程度上提高了安全性
TIP这种方案的大致流程:
1. **登录成功得到两个token,并将其存起来** 1. **当access_token过期的时候,利用refresh_token发送刷新token的请求** 1. **得到新的token之后,需要将请求重新发送,实现用户无感刷新**
主要代码:
TIP 主要在响应拦截器中,进行token的无感刷新,这一步我们需要考虑几个主要问题:
- 既然要做到用户无感,那么当前请求就不能被舍弃,需要在获得新的token之后再帮他执行一次
- 存在并发请求时,可能导致多次刷新token的情况,所以需要一个全局标志位来代表是否正在刷新token,并维护一个队列对请求进行存储
// 当前是否正在刷新token
let isNotRefreshing = true
// 存储请求的队列
let request = []
axios.interceptors.response.use(res => {
// 为了实现需求,我们需要和后端约定一个响应体。比如: {code=10415,msg='token已过期',data:null},当收到token过期的响应就要进行token刷新了
if(res.data.code === 10415){
// 拿到响应的配置对象,这和请求的配置参数是一样的,包括了url,data等相关信息,之后需要使用config进行请求的重发
const config = res.config
if(isNotRefreshing){
isNotRefreshing = false
// 发送刷新token的请求,完全可以将这个操作封装成一个函数比如refreshToken。因为上面已经在请求拦截器中做了判断处理(根据不同请求携带access_token或refresh_token),所以这里就直接发送请求了
return axios.get('/admin/refreshToken').then(res => {
// 如果refresh_token也过期了,那用户只能重新登录 (响应体、状态码请和后端自行约定)
if(res.code === 10422 || res.code === 10415){
// tokenBo就是那两个token的存储对象
localStorage.removeItem("tokenBo")
// 这个是使用 access_token获取的类似用户的相关信息
localStorege.removeItem("currentAdmin")
router.push('/login')
}else if(res.code === 10200){
// token刷新成功后,将新的token存起来
localStorage.setItem("tokenBo",JSON.stringify(res.data))
// 执行request队列中的请求
request.forEach(fn => fn())
// 请求队列执行完毕,置空
request = []
// 重新执行当前未成功的请求并返回
return axios(config)
}
})
.catch(() => {
localStorage.removeItem("tokenBo");
localStorage.removeItem("currentAdmin");
router.push('/')
})
.finally(() => {
isNotRefreshing = true;
})
} else {
//如果当前已经是处于刷新token的状态,就将请求置于请求队列中,这个队列会在刷新token的回调中执行,由于new关键子存在声明提升,所以不用顾虑会有请求没有处理完的情况,这段添加请求的程序一定会在刷新token的回调执行之前执行的
return new Promise(resolve => {
//这里加入的是一个promise的解析函数,将响应的config配置对应解析的请求函数存到requests中,等到刷新token回调后再执行
requests.push(() => {
resolve(axios(config));
})
})
}
} else {
if (res.data.code == 10200) {
return res.data;
} else {
if (res.data.code == 10409) {
localStorage.removeItem("tokenBo");
localStorage.removeItem("currentAdmin");
router.push('/push')
}
Message.error(res.data.message);
return res.data;
}
}
},err => {
if (err && err.response && err.response.status) {
switch (err.response.status) {
case 404:
Message.error("页面未找到");
break;
case 401:
Message.error('没有权限访问')
break;
case 500:
Message.error("系统维护中")
break;
case 505:
Message.error("网络错误")
}
}
})