基于前后端分離的Web應(yīng)用在處理權(quán)限校驗(yàn)方面使用JWT是常用的方式, 如何延長(zhǎng)Token的壽命則是這種方式必須要解決的問(wèn)題。 本文主要介紹在Angular應(yīng)用種如何自動(dòng)刷新Token,實(shí)現(xiàn)Token壽命無(wú)感知延續(xù)。
Angular 實(shí)現(xiàn)自動(dòng)刷新Token主要利用 HttpInterceptor. 這是一個(gè)Interface, 它提供了一個(gè)Intercept() 方法, 我們實(shí)現(xiàn)Token自動(dòng)刷新的核心思路就是在這個(gè)連接器里面判斷如果Token需要被刷新就先hold住原來(lái)的API請(qǐng)求并且先去請(qǐng)求獲取Token的API, 當(dāng)拿到新的Token之后繼續(xù)發(fā)起前面掛起的API請(qǐng)求。
如上圖所示, 我們接下來(lái)要實(shí)現(xiàn)的所有功能都將在 Http Intercepotr 里面實(shí)現(xiàn), 具體步驟如下。
1. 確定刷新token的周期?
與后端同志們約定好Token失效時(shí)間以及何時(shí)刷新token的規(guī)則。比如說(shuō)我們?cè)O(shè)置token過(guò)期時(shí)間為10分鐘,那我們刷新token的時(shí)間首先就必須要小于10分鐘(Refresh Token API 本身也是需要auth驗(yàn)證的,如果token過(guò)期將直接請(qǐng)求失敗),而設(shè)置的刷新token的時(shí)間與token過(guò)期時(shí)間太接近也可能存在問(wèn)題,有可能由于網(wǎng)絡(luò)開(kāi)銷(xiāo)導(dǎo)致發(fā)送到后端的時(shí)候也已經(jīng)過(guò)期。所以我們可以考慮約定5分鐘為刷新token的周期。
2. Token解析與存儲(chǔ)
對(duì)于不了解JWT 的同學(xué)可以自行查看相關(guān)資料,這里不做太多描述。 我們只需要知道登錄成功之后返回的token里面有我們需要的兩個(gè)值—— token生成時(shí)間和過(guò)期時(shí)間。
interface JWT {
????sub: string;
????iat: number; // Token 生成時(shí)間戳
????exp: number; // Token 過(guò)期時(shí)間戳
}
當(dāng)?shù)卿洺晒δ玫絫oken之后,我們除了要將token保存到storage種(localStorage 或者 sessionStorage), 還要同時(shí)在storage里面標(biāo)記拿到token的時(shí)間 tokenLastModify, 這個(gè)值將用于我們?cè)趯?lái)在攔截器里面檢查是否需要刷新token的邏輯中使用到。
3. Http Intercepotr 實(shí)現(xiàn)
前面說(shuō)了那么多只是準(zhǔn)備工作,接下來(lái)將具體介紹攔截器的具體實(shí)現(xiàn)。
-? 創(chuàng)建一個(gè)攔截器,并注入到AppModule 中
refresh-token.interceptor.service.ts
```
? ??export RefreshTokenInterceptor implements HttpIntercetpor {
? ? ? ? ?constructor () {}
? ? ? ? ? ...
? ? ? ? ?intercept(req: HttpRequest, next: HttpHandler): Observable> {?return next.handle(req);?}
}
```
app.module.ts
```
{ provide: HTTP_INTERCEPTORS, useClass: RefreshTokenInterceptor, multi: true },
```
- 實(shí)現(xiàn)intercept 方法如下
```
?intercept(req: HttpRequest, next: HttpHandler): Observable> {?
? ? const? { url } = req;
? ? if (this.shouldRefreshToken && isNotRefreshTokenApi) { // refres token的API 不需要這個(gè)邏輯,直接請(qǐng)求就可以了,否則循環(huán)調(diào)用 refresh token API。
? ? ? ? if (!this.refreshTokenInProgress) {??//refreshTokenInProgress : 這個(gè)變量用于標(biāo)記當(dāng)前是否有獲取token的動(dòng)作在進(jìn)行中,當(dāng)這個(gè)值為true 的時(shí)候其他的API將不會(huì)重復(fù)請(qǐng)求refresh token
? ? ? ? ? ? this.refreshTokenInProgress = true;??
? ? ? ? ? ? this.refreshTokenSubject.next(null);? //??refreshTokenSubject? 置空,攔截當(dāng)前請(qǐng)求;
? ? ? ? ? ? //? 重新獲取token,得到新的token之后, 使用新token發(fā)送原始請(qǐng)求。
? ? ? ? ? ? return this.refreshToken().pipe(
? ? ? ? ? ? ? ? switchMap((resp) => {
? ? ? ? ? ? ? ? ? ? this.storageService.setStorage({??//? 跟新storage
? ? ? ? ? ? ? ? ? ? ? ? ... resp,?
? ? ? ? ? ? ? ? ? ? ? ? tokenLastModify: new Date().getTime(),? // 更新token刷新時(shí)間。
????????????????????});
? ? ? ? ? ? ? ? ? ? this.refreshTokenSubject.next(resp.token);? //??refreshTokenSubject返回非null 值, 放行其他的API
? ? ? ? ? ? ? ? ? ? return next.handle(this.injectTOken(req)); //?injectTOken() 方法實(shí)現(xiàn)替換新token, 這里將使用新token發(fā)送原始請(qǐng)求
? ? ? ? ? ? ? ? ? }),
? ? ? ? ? ? ? ? ? catchError(errorRes => this.handlError(errorRes)),
? ? ? ? ? ? ? ? ? finalize(() => {
? ? ? ? ? ? ? ? ? ? ? ? this.refreshTokenInProgress = false; // 最終必須要還原?refreshTokenInProgress狀態(tài),否則會(huì)阻止后面的請(qǐng)求通過(guò)。
? ? ? ? ? ? ? ? ? })?
????????????)
????????} else {
? ? ? ? ? ? return this.refreshTokenSubject.pipe(
? ? ? ? ? ? ? ? fileter(result => result !== null), // 這個(gè)過(guò)濾器就實(shí)現(xiàn)當(dāng)有請(qǐng)求token的進(jìn)程發(fā)生,掛起其他請(qǐng)求的作用。
? ? ? ? ? ? ? ? take(1),
? ? ? ? ? ? ? ? switchMap(() => {
? ? ? ? ? ? ? ? ? ? return next.handle(this.injectToken(req));
????????????????})
????????????)
????????}
????} else {
? ? ? ? return next.handle(req);
????}
}
```
上面的代碼主要利用 創(chuàng)建了個(gè) rxjs 屬性?refreshTokenSubject 來(lái)實(shí)現(xiàn)我們控制請(qǐng)求是掛起還是繼續(xù)。當(dāng)一個(gè)請(qǐng)求進(jìn)入攔截器,被判斷為需要刷新token,我們利用?refreshTokenSubject.next(null) 來(lái)阻止后面的其他http請(qǐng)求被真正發(fā)出(其他的API 會(huì)進(jìn)入下面的 else 條件中,進(jìn)而被 ?fileter(result => result !== null) 過(guò)濾掉,不執(zhí)行 next.handle()方法), 而當(dāng)前的這次請(qǐng)求會(huì)先請(qǐng)求刷新token的API, 當(dāng)拿到新的token 存入storage并跟新獲得token的時(shí)間戳,在token更新之后? 執(zhí)行 ?this.refreshTokenSubject.next(resp.token),可以告知其他的API token已經(jīng)刷新 ,其他的API 就會(huì)使用新token 繼續(xù)之前的請(qǐng)求。最終,當(dāng)前的請(qǐng)求會(huì)通過(guò)? return next.handle(this.injectTOken(req))帶上新token真正發(fā)送Http請(qǐng)求。
refreshTokenSubject 聲明如下
refresh-token.interceptor.service.ts
```
? ??export RefreshTokenInterceptor implements HttpIntercetpor {
? ? ? ? private refreshTokenInProgress = false;
? ? ? ? private refreshTokenSubject: Subject<any> = new BehaviorSubject<any>(null);
????}
```
shouldRefreshToken 的實(shí)現(xiàn):
```
? ? get?shouldRefreshToken() {
? ? ? ? const token = localStorage.getItem('token');
? ? ? ? if (token) {
? ? ? ? ? ? const tokenObj = JSON.parse(atob(token.splice('.')[1]));
? ? ? ? ? ? const maxAge = (tokenObj.exp - tokenObj.iat) * 1000 - 3000; // 生命周期 - 3秒,留點(diǎn)buffer,避免 token太小的極端情況導(dǎo)致過(guò)期。
? ? ? ? ? ? const shift = Math.floor(maxAge / 2); // 刷新token時(shí)間設(shè)定為token過(guò)期時(shí)間的一半
? ? ? ? ? ? const lastModify = localStorage.getItem('tokenLastModify');
? ? ? ? ? ? if (lastModify) {
? ? ? ? ? ? ? ? return (new Date().getTime() - Number(LastModify) > maxAge - shift);?
????????????} else {
? ? ? ? ? ? ? ? return true;
????????????}
? ? ? ? } else {
? ? ? ? ? ? return true;
????????}
????}
```