基于Angular Http Interceptor(攔截器)實(shí)現(xiàn)自動(dòng)刷新Token

基于前后端分離的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;

????????}

????}

```


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,115評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 177,577評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,514評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,234評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,621評(píng)論 1 326
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,822評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,380評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,128評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,319評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,548評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,970評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,229評(píng)論 1 291
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,048評(píng)論 3 397
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,285評(píng)論 2 376

推薦閱讀更多精彩內(nèi)容