刷新重试策略
如果 API 返回 access_token
失效,那么从 API 获取新的 access_token
,并重试失败的请求。
如果 refresh_token
也失效,让用户去登录。
实现过程
使用 Angular 的拦截器可以拦截 API 请求,可以在请求前后做些处理。要使用拦截器,只需要实现 HttpInterceptor
接口的 intercept
方法。一个不做任何处理的拦截器是下面这个样子。
示例:
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class NoopInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<any>> {
return next.handle(req);
}
}
token 刷新就是在 intercept
方法里面做的。
可以在请求发出前对 HttpRequest
进行处理,比如获取请求头中的数据或者更改请求等等。
示例:
intercept(req: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<any>> {
const token = req.headers.get('Authorization');
console.log(token);
return next.handle(req);
}
intercept
方法返回的是一个 Observable
。因此,对 response
做后置处理可以使用 rxjs6 的 pipe
。
示例:
intercept(req: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<any>> {
return next.handle(req).pipe(some code...);
}
我在项目中,对 Angular 的 HttpClient
做了一次封装,对外提供了一个 HttpService
。 请求头的前置处理,比如设置 token
等的处理放在了 HttpService
中了:
示例:
...
let headers = new HttpHeaders();
headers.append('Authorization', ''[token]);
...
所以在处理 token
刷新的拦截器中,只需判断 API 请求是否设置了 token
信息就可以筛选出需要处理的请求了,需要处理的对 response Observable
使用 pipe
,不需要的就直接返回 next.handle(req)
。
处理过程用到了 rxjs 的几个操作符:map
、tap
、concatMap
、retryWhen
、delay
,前期投入的时间也主要是花在了这些操作符上,rxjs 的数据流操作方式与以往开发经验还是有很大不同,需要思维的转换。
复习下这几个操作符。
map
:对源Observable
发出的值应用处理,并将结果作为Observable
发出,常用的操作符,相当于promise
的then
;tap
:没有副作用的进行一些透明地操作,比如打印日志、数据采集可以使用这个操作符;concatMap
:这个与map
的区别:map
订阅后输出Observable
,订阅concatMap
输出值;
示例:
becomeHero() {
return of('hero');
}
const source = of(1);
source.pipe(
map(() => this.becomeHero())
).subscribe(res => {
console.log('map:', res); // 输出:map:Observable {_isScalar: true, _subscribe: ƒ, value: "hero"}
});
source.pipe(
concatMap(() => this.becomeHero())
).subscribe(res => {
console.log('concatMap:', res); // 输出:concatMap:
hero
});
retryWhen
:出错后重试修改后的源;delay
:延迟源 Observable 的发送。
按照刷新策略,Angular 中使用 rxjs 实现的重试逻辑如下所示。在 Angular 中间件拦截器中,先筛选出需要刷新控制的请求。这里使用 rxjs 的 map
操作符判断此请求是否需要刷新,需要刷新的抛出错误,以被 retryWhen
操作符接收做重试处理,否则,返回正确的结果。由于是 token
失效的请求重试,所以 retryWhen
里面需要获取新的 token
并重新设置到此请求当中,否则依然用旧的 token
尝试请求是一定会失败的。这里是放在了 tap
操作符当中处理的。然后在 map
操作符当中控制下刷新的次数。最后通过 delay
操作符设置下重试的时间间隔,整个逻辑就完成了。
示例:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
// 判断是否是需要校验的请求,不是的直接放行
if (!req.headers.get('Authorization')) {
return next.handle(req);
}
const response = next.handle(req)
.pipe(
map((res: HttpResponse<any>) => {
// 判读是否需要重试,抛出错误的请求会进入 retryWhen 走重试逻辑,否则返回结果
if (Lhas(res, 'body.status') && Lget(res, 'body.status') === 401) {
throw res;
}
return res;
}),
retryWhen(error => {
return error
.pipe(
concatMap(() => this.handleToken()), // handleToken 是获取 token 的逻辑,代码贴到下面
tap(token => {
// 重新设置 token
const headersMap = new Map();
headersMap.set('authorization', [`${token}`]);
Lset(req, 'headers.headers', headersMap);
return req;
}),
map((res, count) => {
// 重试次数控制
if (count > 0) {
throw res;
}
return res;
}),
delay(1000), // 延迟重试
);
}
)
);
上面代码中获取 token 的处理逻辑贴在下面。这段代码逻辑处理了一个页面中多个请求都 token
失效的情况,这种情况下会造成发送多个 refreshToken
请求,这样不仅仅会造成资源的浪费,最重要的是可能会导致第一个获取到 token
的请求重试时,第二个 refreshToken
请求也成功了,然后第一个重试就因 token
被刷新调而失败了。这里是通过定义一个刷新时间间隔(timeDebounce
)来控制的。将 token
放在本地存储中(cookie 或者 localStorage),如果是间隔时间内的请求就直接本地取 token
返回,超过间隔时间的请求才从网络获取 token
。间隔时间需要根据页面接口调用数量及处理时间来确定,比如设置为10秒的意思是所有的请求都会在10秒内发出,这几个请求都设置了新的 token
并发出了请求。
示例:
/**
* 刷新 token
* @param req
*/
handleToken(): Observable<any> {
const currentTime = new Date().getTime();
const accessTokenFromLocal = getAccessTokenFromLocal();
const reGetTokenFromNet = this.refreshToken() // 请求接口,刷新 token
.pipe(
map(this.saveToken.bind(this))
);
// 没有 refreshTokenTime, 或
// 没有 accessTokenFromLocal,或
// 重试间隔大于 timeDebounce 秒,从网络获取,并记录时间
if (!this.refreshTokenTime ||
!accessTokenFromLocal ||
currentTime - this.refreshTokenTime > this.timeDebounce) {
this.refreshTokenTime = currentTime;
return reGetTokenFromNet;
} else {
return accessTokenFromLocal;
}
}
到此为止,整个 token
的刷新重试流程就完成了。
发表回复