一、问题背景:
上面绕口的标题不知道大家看不看的懂。通常我们用拦截器就是两个目的,
1、在请求头里统一添加请求头。
2、对响应结果预先处理。
我现在项目就是利用拦截器,在请求头里增加:'Authorization': this.storage.token 的请求头。
// 最精简的一个拦截器 。一会儿 会在这个代码基础上增加后续讨论的代码
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const request = req.clone({
setHeaders: {
'Authorization': this.storage.token,
}
});
return next.handle(request);
}
现在问题升级一下: token有一个固定的失效时间,30分钟。这样用户在连续使用系统时,一旦登录时间到30分钟,token就失效了,回到登录页面,体验很不好。 那么如何监测用户是在“连续活动”的时候,且当前token超时后,系统能自动获取新token,并且在之后请求中使用该新token呢? 简化一下表述:如何在拦截里中,判断token失效了能自动请求新token,并且把新token赋予当前的拦截请求中去。 其实这个事情要解决2个问题:
1、时间的判定逻辑: 判断当前时间与 用户的上次活动时间和获取token的时间, 决定是让用户重登录,还是我的程序自动更新一下token,让用户继续访问系统。
2、拦截器异步注入一个请求:如何在拦截器里,加入一个异步请求token的操作 。
二、时间的判定逻辑
时间判定的逻辑不难,我只要在localstorage里保存一下登录时间 和用户最近一次发出过请求的时间 即可。 我保存一个时间对象:{"token":1534312524914,"active":1534312524914} 来记录时间。
export interface IStoredTime {
token: number;
active: number;
}
// 全局的存储服务
@Injectable({ providedIn: 'root' })
export class AuthStorageService {
private _timeKey = 'ss_tokenTime';
get time(): IStoredTime {
const str = localStorage.getItem(this._timeKey);
return str ? JSON.parse(str) : null;
}
set time(v: IStoredTime) {
localStorage.setItem(this._timeKey, JSON.stringify(v));
}
}
当用户登录时记录保存时间:
// 登录后,立即保存时间
const time = +new Date();
this.storage.time = { token: time, active: time };
现在在拦截器里增加时间判定的业务的代码,针对三种情况,分别处理一下就好了:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let request = req;
// 如果是请求login,reToken
if (req.url === refreshTokenURL || req.url === loginUrl) {
return next.handle(request);
}
// 判断时间
const time = this.storage.time;
if (time) {
const now = +new Date();
const interval = 30 * 60 * 1000; // 30分钟的token失效时间,也是用户不活动的最大时间
if (now - time.active >= interval) {
// 此时用户已经是不活动用户了,直接跳转登录页面
} else if (now - time.token >= interval) {
// 此时用户仍然是活动的,但要更新一下token
} else {
// 正常请求,更新活动时间,并继续拦截器的流程
this.storage.time = { ...time, active: now };
request = req.clone({
setHeaders: {
'Authorization': this.storage.token,
}
});
return next.handle(request);
}
}
}
三、拦截器里注入一个异步请求
这个是难处理的,因为当前拦截器急迫的需要你返回一个Observable对象,但你需要先异步走,请求到新token后, 把新token应用回当前拦截器。 异步请求token也会走拦截器。
思路一: 同步http请求新token。 我翻了ng的HttpClient文档,没找到同步的参数,像jquery.ajax 传入 {async:false} 这种。如果ng中有同步请求的方法,我认为它是可行的。如果有人知道同步怎么写,可以在下面留言。
思路二:委托一个新的Observable对象,接力实现。
1、既然当前拦截器需要返回一个Observable对象,我就先new一个Subject给拦截器,让它先返回一个Subject.
2、此时我就放心去异步请求新token,请求后,将新token赋于拦截器的自己的业务请求上。
3、当业务请求返回结果后,再触发第一步的Subject对象的next的方法。
此过程对用户无感的,默默地更新了token,他/她又可以愉快的玩耍30分钟了。
思路二的代码如下:
jumpLogin(msg) {
this.router.navigate(['/login']);
}
// 重新获取token ,这里用了await来装13,其实可以用正常的subscribe()回调获取新token数据
async reTokenAsync(req: HttpRequest<any>, next: HttpHandler, sub: Subject<any>) {
const reTokendData = await this.hc.post<any>(refreshTokenURL, { oldToken: this.storage.token }).toPromise();
const now = +new Date();
this.storage.time = { token: now, active: now }; // 更新时间 和 新token
this.storage.token = reTokendData.refreshToken;
const request = req.clone({
setHeaders: {
'Authorization': this.storage.token,
}
});
// 让真的业务请求重现江湖
next.handle(request).pipe(
tap(data => {
sub.next(data); // 数据到达,转达下发
return data;
}, (error) => {
sub.error(error); //数据报错,转达出错
}
)
).subscribe(); //由于该Observable对象已经没有人去主动订阅它了。所以我们手动订阅一下,极重要!!!!
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let request = req;
// 如果是请求login,reToken
if (req.url === refreshTokenURL || req.url === loginUrl) {
return next.handle(request);
}
// 判断时间
const time = this.storage.time;
const subject = new Subject<any>(); // 被委托的对象
if (time) {
const now = +new Date();
const interval = 30 * 60 * 1000; // 30分钟的token失效时间,也是用户不活动的最大时间
if (now - time.active >= interval) { // 不活动用户了,直接跳转
this.jumpLogin();
// 也返回个对象 。但它不会有地方触它
return subject;
} else if (now - time.token >= interval) { // 活动的,需更新一下token
this.reTokenAsync(req, next, subject);
// 返回被委托的对象 。让真正的业务请求隐匿起来。
return subject;
} else { // 正常请求,更新活动时间,并继续拦截器的流程
this.storage.time = { ...time, active: now };
request = req.clone({
setHeaders: {
'Authorization': this.storage.token,
}
});
return next.handle(request);
}
}
}
思路二的核心有二:
一是在拦截器里创建一个 new Subject
其次是在重新获取token后,让原业务请求重新发生,并用要subscribe()一下。
=========================================================
这个问题解决的有点绕的,网上也搜索不到相关的技术文章。
这个问题最根本的原因是不要设计token这种验证的机制,应该用session来做。
不过我也趁此机会,探索一下拦截器中的异步请求问题,在其它时候没准用的着吧
我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite\_code=1pgwko43rna2v