Harmonyos next:网络请求token过期自刷新 & 对比Android场景

499 阅读4分钟

token用于网络请求的服务器鉴权。由于token存在有效期,到期后如果不刷新接口就不会正常响应。app需要实现在用户侧无感知的自刷新token。

一、通用方案

方案是在网络框架的拦截器拦截响应,如果判断到token过期(一般和服务器约定code值或者有token过期时间戳),就重新请求token,并且把当次请求的token换成新token,重新发起请求,这样view那边拿到的数据就是正常的新token返回的数据,对用户也没有影响。

需要注意的点是,如果同时有多个请求过来,并且都token过期,都需要换新token重新发起请求,就可能造成刷新token的请求重复发多次。但实际上只需要一次刷新token,后续的直接替换新token即可。

这个方案是通用的,在鸿蒙和安卓上都是这个思路,只是实现上稍有不同。

区别一:鸿蒙的网络框架axios拦截器和安卓okhttp的拦截器使用方式不同

区别二:鸿蒙axios发起请求默认是ts的promise机制,在一个线程上通过任务队列,类似安卓的协程;安卓okhttp请求默认是在线程池上面(cache线程池)

二、鸿蒙上的实现

2.1拦截器:
//请求拦截器
instance.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
  return new Promise((resolve,reject)=>{
    
  })
})
​
//响应拦截器
instance.interceptors.response.use((response: AxiosResponse) => {
  return new Promise((resolve, reject) => {
​
  })
})

这个拦截器不同于安卓的interceptor责任链模式可以拆开分成一个个的拦截器,鸿蒙目前使用上是当成一个拦截器。比如header上加公共参数,token刷新,日志打印等在安卓上可以分成多个拦截器,目前在鸿蒙上都是写在一起的。

2.2检测token过期后自刷新:

在response拦截器里面,判断响应code,符合token过期的场景就发起刷新token的请求后替换config参数重新发起请求返回response,代码如下

instance.interceptors.response.use(async (response: AxiosResponse) => {
  if (response.status == 200 && Number(response.data.code) === 10086111) {
    //这里code 10086111表示token过期或失效
    //同步刷新token
    let newToken = await refreshTokenSync()
    let config: InternalAxiosRequestConfig = response.config
    //替换请求token参数
    config = replaceTokenStr(config, newToken)
    try {
      //重新发起请求
      const refreshResponse: AxiosResponse = await axios.request(config)
      return refreshResponse
    } catch (e) {
      Logger.info("refreshResponse error = "+config.url+"   " + JSON.stringify(e))
    }
  }
  return response
}, (error: AxiosError) => {
  return Promise.reject(error);
})
2.3处理同时多个请求触发多次token刷新

按上面的代码有个问题就是前文提到的,refreshTokenSync刷新token的方法会调用多次,理论上只需要第一个请求刷新就可以。因此需要改进。

思路是第一个请求在执行时,后续的请求阻塞住,等第一个请求结束后,后续的请求再执行后面的代码,检查是否可以直接用新的token跳过刷新token请求。

在多线程的场景,比如安卓上用同步代码块(当一个线程进入同步代码块,其他线程需要等那个线程的信号才能进入同步代码块)可以比较方便的实现这个逻辑,但是在鸿蒙上的单线程异步没有同步代码块的概念。因此换一个思路,在有方法在执行token刷新时,其他方法进入队列,等toekn刷新完成后,从队列里面重放这些请求。

export interface tokenCallBack {
  config: InternalAxiosRequestConfig<object | string>,
  func: Function
}
let retryList = new ArrayList<tokenCallBack>()
let isRefreshing = false//拦截器里面如果isRefreshing,其他token过期请求全部进入retryList
instance.interceptors.response.use((response: AxiosResponse) => {
  return new Promise((resolve, reject) => {
    if (response.status == 200 && Number(response.data.code) === 10086111) {
      const tokenBack: tokenCallBack = {
        config: response.config,
        func: resolve
      }
      retryList.add(tokenBack)
      if (!isRefreshing) {
        isRefreshing = true
        handleRefresh()
      }
    } else {
      resolve(response)
    }
  })
})
​
function handleRefresh() {
   refreshTokenSync().then((newToken: string) => {
    isRefreshing = false
    //刷新token完成成,重放所有需要retry的请求
    retryList.forEach((callback: tokenCallBack) => {
      let config = callback.config
      config = replaceToken(config)
      axios.request(callback.config).then((data: object) => {
        callback.func(data)
      }).catch((e: Error) => {
        Logger.warn("替换token后失败,url=" + config.url +" e="+JSON.stringify(e))
      })
    })
    retryList.clear()
  })
}

三、安卓上的实现

安卓上的实现就简单描述下,注意突出差异点

3.1拦截器

自定义一个okhttp的拦截器取名tokenRefrehInterceptor,实现后在okhttp初始化时加入到okhttp的拦截器列表里面。

3.2检测token过期后自刷新

这块和鸿蒙一样,读取response,判断code。

稍微需要注意的是不能直接把body.toString(),不然后面response想再调用toString就会异常。解决办法是参考okhttp自带的logInterCeptor的实现,把response解析出来。

3.3处理多次token刷新

如前文提到,由于安卓okhttp是多线程请求,可以通过sync同步代码块,然后在同步代码块里面doubleCheck检查现在的token是否能使用,如果能使用直接替换token。

类似单列模式的doubleCheck写法。

四、总结

这个方案是通用的,在鸿蒙和安卓上都是这个思路,只是实现上因为各自语言和运行环境不一样有所不同。

我这边是把安卓的方案移植到鸿蒙上,根据鸿蒙和安卓的差异,在鸿蒙上做了一些调整。

有个大前提是,不管技术测如何实现,在用户侧的效果要是(尽量)一样的。