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 多語系實務應用
0 意見:
留言經過版主審核後才會出現哦!
因為不這樣做會有很多垃圾留言,不好意思 > <