【Angular】如何在token失效並重新登入後,重發前次Request?


Angular如何在access token和refresh token都失效的情況下,跳出視窗重新登入後繼續發送原本的Request?

在Angular使用OAuth的驗證通常會在HttpInterceptor使用,
這樣一來在整個App中所有發出的HttpRequest都可以很方便的在headers上加上access token。

當然,如果你有一些request是不想驗證的,例如網站上有一些東西即使不用登入也能看的東西,就可以用HttpBackend來避掉HttpInterceptor。


使用情境與時機
現在來直接說明目前碰到的問題,假設現在有一個頁面A,
這個頁面需要登入後才能使用,使用者按下查詢按鈕後發送request查詢資料,使用async方式呼叫service api,
所以在component上可能會長這樣:
<ng-container *ngIf="formFo$ | async as formFo; else loading">
   // 根據拿回來的資料長出table或form之類的
</ng-container>
<ng-template #loading>
    讀取中...
</ng-template>


一般的情況下,
在使用者登入完後會從authorization server取得access token和refresh token,並把這些資訊存到localstorage上。


如果今天access token已經失效了,那麼在使用者發request時會發現認證失效而失敗,此時應使用refresh token去取得新的access token,並把剛剛發失敗的request重發。
這樣一來使用者在操作畫面上不會發現什麼異狀,資料也能很順暢的顯示出來。
可是如果今天連refresh token也失效該怎麼辦呢? 一般的做法是強迫使用者登出並直接導頁到登入畫面來處理,
但如過今天使用者在頁面上填寫許多欄位的表單,直接登出的話可能會覺得很幹,剛剛填的東西全沒了(假設沒有暫存功能),所以現在要用一種方式讓使用者保留在原畫面,又能重新登入不讓資料遺失!





做法
下面都是在你已經能能做出refresh token效果後的做法,如果還不知道怎麼做的可以先參考這一篇文章
http://bit.ly/2uHzfGc

在HttpInterceptor上,呼叫refreshToken失敗時先開啟一個dialog,讓使用者輸入帳號密碼後登入
如果登入的人和前一個相同,則把剛剛失敗的request再重發一次。

最終要達到的效果 :

如果重新登入的人和前一個不同,那麼就不重發request了。


HttpInterceptor程式 : 
handle401Error(request: HttpRequest<any>, next: HttpHandler) {
        /*
            參考程式:
            https://www.intertech.com/Blog/angular-4-tutorial-handling-refresh-token-with-new-httpinterceptor/
        */

        /*
            Note that no matter which path is taken,
            we must return an Observable that ends up resolving to a next.handle call
            so that the original call is matched with the altered call
        */
        if (this.isRefreshingToken === false) {
            this.isRefreshingToken = true;

            // Reset here so that the following requests wait until the token comes back from the refreshToken call.
            this.tokenSubject.next(null);

            const response =  this.oauth2Service.refreshTokenFromOauthServe().pipe(
                switchMap(token => {
                    if (token) {
                        this.oauth2Service.saveTokenToLocalStorage({
                            access_token: token.access_token,
                            refresh_token: token.refresh_token,
                            user_name: token.userInfo.name,
                            user_id: btoa(token.userInfo.id)
                        });
                        this.tokenSubject.next(token);
                        return next.handle(this.applyCredentials(request));
                    }

                    // If we don't get a new token, we are in trouble so logout.
                    return this.logoutUser();
                }),
                catchError((error) => {
                    // If there is an exception calling 'refreshToken', bad news so logout.
                    this.dialogRef = this.dialog.open(LoginDialogComponent, {
                        width: '300px',
                        autoFocus: true,
                        hasBackdrop: true,
                        disableClose: true
                    });

                    this.dialogRef.afterClosed().subscribe((newToken: Oauth2Token) => {
                        if (newToken) {
                            this.tokenSubject.next(newToken);
                        }
                    });

                    return this.tokenSubject.pipe(
                        filter(token => token != null),
                        take(1),
                        switchMap((token) => { 
                            if (這邊看你自己要用什麼方法判斷本次登入者和前一次是否相同) {
                                this.oauth2Service.saveTokenToLocalStorage({
                                    access_token: token.access_token,
                                    refresh_token: token.refresh_token,
                                    user_name: token.userInfo.name,
                                    user_id: btoa(token.userInfo.id)
                                });
                                this.router.navigateByUrl('/resume');
                                return EMPTY;
                            } else {
                                this.oauth2Service.saveTokenOnlyToLocalStorage({
                                    access_token: token.access_token,
                                    refresh_token: token.refresh_token,
                                });
                                return next.handle(this.applyCredentials(request));
                            }
                        })
                    );
                }),

                finalize(() => {
                    /*
                        When the call to refreshToken completes, in the finally block,
                        reset the isRefreshingToken to false for the next time the token needs to be refreshed
                    */
                    this.isRefreshingToken = false;
                })
            );
            return response;
        } else {
            /*
                If isRefreshingToken is true, we will wait until tokenSubject has a non-null value – which means the new token is ready
                Only take 1 here to avoid returning two – which will cancel the request
                When the token is available, return the next.handle of the new request
            */
            return this.tokenSubject.pipe(
                filter(token => token != null),
                take(1),
                switchMap(() => {
                    return next.handle(this.applyCredentials(request));
                })
            );
        }
    }

最一開始寫的時候是在catchError return EMPTY,導致整個資料流complete,應該是要等待dialog關閉後,再決定是要終止還是繼續執行下去。


結論:
主要就是要讓Observable等待,等到重新登入完後再繼續發request。


其他Angular相關:

【Angular】ngx-translate 多語系實務應用

Next
Previous
Click here for Comments

0 意見:

留言經過版主審核後才會出現哦!
因為不這樣做會有很多垃圾留言,不好意思 > <