上面繞口的標題不知道你們看不看的懂。一般咱們用攔截器就是兩個目的,javascript
一、在請求頭裏統一添加請求頭。java
二、對響應結果預先處理。jquery
我如今項目就是利用攔截器,在請求頭裏增長:'Authorization': this.storage.token 的請求頭。ajax
// 最精簡的一個攔截器 。一下子 會在這個代碼基礎上增長後續討論的代碼 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個問題:session
一、時間的斷定邏輯: 判斷當前時間與 用戶的上次活動時間和獲取token的時間, 決定是讓用戶重登陸,仍是個人程序自動更新一下token,讓用戶繼續訪問系統。異步
二、攔截器異步注入一個請求:如何在攔截器裏,加入一個異步請求token的操做 。 async
時間斷定的邏輯不難,我只要在localstorage裏保存一下登陸時間 和用戶最近一次發出過請求的時間 便可。 我保存一個時間對象:{"token":1534312524914,"active":1534312524914} 來記錄時間。ide
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)); } }
當用戶登陸時記錄保存時間:post
// 登陸後,當即保存時間 const time = +new Date(); this.storage.time = { token: time, active: time };
如今在攔截器裏增長時間斷定的業務的代碼,針對三種狀況,分別處理一下就行了:this
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對象,接力實現。
一、既然當前攔截器須要返回一個Observable對象,我就先new一個Subject給攔截器,讓它先返回一個Subject.
二、此時我就放心去異步請求新token,請求後,將新token賦於攔截器的本身的業務請求上。
三、當業務請求返回結果後,再觸發第一步的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<any>(); 而後返回它。
其次是在從新獲取token後,讓原業務請求從新發生,並用要subscribe()一下。
=========================================================
這個問題解決的有點繞的,網上也搜索不到相關的技術文章。
這個問題最根本的緣由是不要設計token這種驗證的機制,應該用session來作。
不過我也趁此機會,探索一下攔截器中的異步請求問題,在其它時候沒準用的着吧
個人博客即將搬運同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=1pgwko43rna2v