访问凭证在前端的维护与续期(oauth2)

912 阅读4分钟

oauth2里有两种token:一种是访问token(access_token,时效一般比较短),另一种是刷新token(refresh_token,时效性一般长一些)。接口在请求一些受限资源时需要携带access_token;当access_token过期时通过refresh来更新,一切都很合理,然而依然存在携带过期access_token接口请求的风险。因此,处理好token的维护与续期就显得特别重要了。本文就探讨一下这个话题

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

如何知道access_token过期了?

一种最简单的方式是添加响应拦截,在拦截器里判断响应的状态码,一般为401未授权表示token无效,这时我们就可以发起refresh操作,刷新完毕再重试该接口即可:

interceptors.response.use(
  function (response) {},
  function (error) {
      //401为异常响应,走这里
      if(statusCode === 401){
          return request('/refresh_token',{refresh_token:'xxxxxx'}).then(res=>{
              //token刷新成功,重试之前失败的接口
              if(res.data.success === true){
                  //直接从error对象里取出失败接口的请求配置信息(error.config)
                  return request(error.config)
              }else{
                  //token刷新失败,做一些特殊处理吧
              }
          }).catch(error=>{
              //token刷新出错,做一些特殊处理吧
          })
      }
    console.log('http-error', error)
    return Promise.reject(error)
  },
)

okay,我们处理了401的情况并增加了接口重试,需要注意避免进入死循环: 一个401的接口导致了refresh行为,refresh行为也报了401的错误,再次触发refresh行为,周而复始...。所以需要跟server端协商好接口的表征,或者将refresh请求排除401拦截之外

看起来好像没什么问题,不过我们忽略了接口的并发性:在触发refresh后和refresh成功前,可能会发起其他的接口请求,这些接口依然会报401,他们会重复refresh的动作

我们稍微修改一下代码:

//刷新状态
let isRefreshing = false;
//记录刷新进行时并发接口resolve函数的队列
const queueWhenRefresh = [];
interceptors.response.use(
  function (response) {},
  function (error) {
      //401为异常响应,走这里
      if(statusCode === 401){
          if(!isRefreshing){
              isRefreshing = true;
              return request('/refresh_token',{refresh_token:'xxxxxx'}).then(res=>{
                  //修改状态
                  isRefreshing = false;
                  //token刷新成功,重试之前失败的接口
                  if(res.data.success === true){
                      //处理等待的接口
                      queueWhenRefresh.forEach(item=>item.resolve())
                      
                      //直接从error对象里取出失败接口的请求配置信息(error.config)
                      return request(error.config)
                  }else{
                      //token刷新失败,做一些特殊处理吧
                  }
              }).catch(error=>{
                  //修改状态
                  isRefreshing = false;
                  
                  //token刷新出错,做一些特殊处理吧
              })
          }else{
              return new Promise((res)=>{
                  queueWhenRefresh.push(res);
              }).then(()=>{
                  //重试接口
                  return request(error.config)
              })
          }
      }
    console.log('http-error', error)
    return Promise.reject(error)
  },
)

这样,我们就不会同时发起多个refresh请求了。

看起来好像挺好的,但是感觉怪怪的,通过接口响应码来判定token失效总体比较被动,我们能不能在接口发起时就先获取token的有效性,有效时再发起请求?

答案是可以的。

提前检测访问凭证的有效性

服务端在返回token时会告诉我们这个token在多久后会过期,我们会将它存储起来。使用之前比对一下时效性。

我们其实可以在请求拦截里添加异步拦截:

request.interceptors.request.use(function (config) {
  const tokenStatus = await getTokenStatus()
  //token有效
  if (tokenStatus === true) {
    return config
  } else {
    return Promise.reject('err-----')
  }
},function(err){})
function getTokenStatus(){
    return new Promise((res,rej)=>{
        const isValid = Number(localStorage.getItem('access_expiretimestamp')) > Date.now();
        if(localStorage.getItem('access_token') &&  isValid){
            res(true);
        }else{
            res(request('/refresh_token',{refresh_token:'xxxxx'}).then(res=>{
                if(res.data.success){
                    return true;
                }
            }))
        }
    })

}

好多了不是吗?什么,主动性还是不够?我们应该保证用户活跃期间token始终保持良好的有效期

  • 监听UI事件,启动刷新。
  • 启动一个定时任务刷新token

想法很好,然而上述动作在并发的情况下会产生新的问题:一次刷新和一个正常发起的请求同时进行。由于服务端接到了刷新请求将token作废,导致后面的接口返回了401,虽然这个接口请求时的token是合法有效的。

怎么破解这个问题,其实把第一个方案里的队列思想搬过来就可以了,当执行刷新时,接口请求一律缓存,待刷新结束再执行请求。具体细节不再赘述。

最后

感谢阅读,如有任何问题欢迎留言讨论!