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写法。
四、总结
这个方案是通用的,在鸿蒙和安卓上都是这个思路,只是实现上因为各自语言和运行环境不一样有所不同。
我这边是把安卓的方案移植到鸿蒙上,根据鸿蒙和安卓的差异,在鸿蒙上做了一些调整。
有个大前提是,不管技术测如何实现,在用户侧的效果要是(尽量)一样的。