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