Springboot+Axios雙token解決token過期續簽問題

前後端分離使用token進行登入驗證時,由於token存在過期時間,每次token過期都需要使用者重新登入的話,使用者體驗很不友好。假如token能跟session一樣,如果使用者持續在進行操作,就自動延長有效時間,就可以解決問題。但是,token一旦簽發,伺服器就沒法再延長token的有效期,目前用的比較多的應該是使用雙token實現token續簽,當token過期時,簽發新的token給前端,前端攜帶新的token請求後端介面。

具體思路:

在簽發token時生成兩個token,accessToken和refreshToken,前端每次請求時攜帶accessToken,後端發現accessToken過期時,返回token已過期的結果。前端根據後端狀態碼判斷token是否已經過期,如果過期則攜帶refreshToken請求重新整理token的介面,如果refreshToken沒有過期,則後端重新生成accessToken和refreshToken返回給前端;如果refreshToken也過期了,則返回結果要求前端重新登入。

後端實現

accessToken設定過期時間為30分鐘,refreshToken設定過期時間為60分鐘,這樣的話accessToken過期後的30分鐘內使用者有操作,仍可以使用refreshToken請求重新整理token。

Springboot+Axios雙token解決token過期續簽問題

建立accessToken

Springboot+Axios雙token解決token過期續簽問題

建立refreshToken

JWT解析token,token過期則返回-1,其他解析錯誤則返回-2,解析成功返回1。

Springboot+Axios雙token解決token過期續簽問題

LoginController

驗證賬號密碼成功後,建立accessToken和refreshToken返回前端,將accessToken和refreshToken儲存在redis中。為避免使用者退出登入或更換裝置登入後,舊的accessToken和refreshToken還沒有過期,仍然能生效,在redis中使用user的id為鍵儲存的accessToken和refreshToken,每次登入後都會將原來的進行覆蓋,這樣只需要在攔截器中將token與reids中進行比對,如果比對不一致,則不放行。

Springboot+Axios雙token解決token過期續簽問題

登入生成accessToken和refreshToken

LoginInterceptor

驗證accessToken,如果返回-1,則表示accessToken過期,提示使用者需要重新整理token。為了避免使用者在新裝置登入,舊裝置的accessToken仍然有效,每次校驗完accessToken成功,都還要在redis中查詢是否存在以id為key的記錄,並且將redis中取出的redisToken和accessToken對比是否一致,如果沒有或不一致,則表示accessToken已經被redis作廢,仍不能放行,返回客戶端資訊為該賬號已在其他裝置登入,請重新登入。

Springboot+Axios雙token解決token過期續簽問題

程式碼截不全,主要邏輯都在

refreshToken介面。

重新整理token的介面/api/refresh用於前端呼叫。首先重新整理token的介面要在Interceptor中放行,避免refreshToken過期後,返回結果仍然是需要重新整理token。只有refreshToken解析成功並且與redis中的refreshToken一致時,才會重新簽發accessToken和refreshToken。

Springboot+Axios雙token解決token過期續簽問題

重新整理token的介面

前端實現

前端實現靠axios的請求攔截器和響應攔截器,請求攔截器配置每次請求攜帶token,主要的難題在於多請求下響應攔截器的處理。

具體思路:

設定一個重新整理token的狀態isRefreshAvailable並設定為true,同時發出多個請求時,token過期都會由後端返回需要重新整理token的資訊,那麼,當第一個響應回來進入重新整理token程式後,將isRefreshAvailable設定為false,其他請求都不能再發起重新整理token請求,使用promise將剩下的請求放入一個快取陣列,當重新整理token結束後再遍歷陣列將快取的請求逐個發出。

由於前端知識不足,網上查了不少辦法,主要出現兩個問題,問題的分析不知道是否正確,最後用了setTimeout延遲3秒再將isRefreshAvailable設定為true並且重新發送快取的請求,沒發現再出現以下兩個問題。

如果有好的解決辦法,請不吝賜教,萬分感激

問題1、重新整理token介面多次被呼叫

。呼叫了7個請求系統時間介面的請求,按照網上的辦法,呼叫重新整理token介面得到新的accessToken和refreshToken後就將isRefreshAvailable設定為true,但有的原始請求響應晚於重新整理token的請求響應,造成多次呼叫重新整理token介面,而後端即便token解析成功也會從redis中進行比對,造成重發的請求攜帶的accessToken與redis中不一致,比對失敗返回重新登入頁面。解決問題的關鍵在於何時改變isRefreshAvailable的狀態。

問題2、請求丟失的問題。

原始請求因為返回結果較晚,當重新整理完token開始遍歷快取陣列的時候,有的原始請求結果才返回,這樣即便進了陣列,也沒有能夠重新發送。

Springboot+Axios雙token解決token過期續簽問題

傳送了7個系統時間請求,重新整理token後只重發了2個

前端程式碼:

accessToken和refreshToken存放在sessionStorage中,獲取accessToken和refreshToken的以及清空sessionStorage到登入頁面的函式:

Springboot+Axios雙token解決token過期續簽問題

function getAccessToken () { return window。sessionStorage。getItem(‘token’)}function getRefreshToken () { return window。sessionStorage。getItem(‘refreshToken’)}function toLogin () { setTimeout(() => { window。sessionStorage。clear() isRefreshAvailable = true requestAttr = [] window。location。href = ‘/login’ }, 3000)}

重新整理token的函式:

獲得重新整理後的accessToken和refreshToken後,儲存到sessionStorage中,得到新的token後這裡先不設定isRefreshAvailable為ture。

Springboot+Axios雙token解決token過期續簽問題

重新整理token函式

async function refreshToken () { try { var result = await http({ url: ‘/test/refresh’, method: ‘post’, headers: { Refresh: getRefreshToken() } }) } catch (e) { messageOnce。error({ message: ‘自動獲取授權失敗! 3秒後自動跳轉至登入介面’ }) toLogin() } if (result。status === 200) { window。sessionStorage。setItem(‘token’, result。accessToken) window。sessionStorage。setItem(‘refreshToken’, result。refreshToken) }}

請求攔截器:

每次請求都在請求頭中設定Authorization欄位攜帶token,這裡使用了element ui的Loading載入元件,為了確保所有的ajax請求響應後再關閉Loading,使用了loadCount進行計數,每發起一個請求,loadCount加1。

Springboot+Axios雙token解決token過期續簽問題

請求攔截器

http。interceptors。request。use( config => { var token = getAccessToken() token && (config。headers。Authorization = token) loadCount++ loadingInstance = Loading。service({ text: ‘正在載入。。。’ }) return config }, error => { loadingInstance。close() messageOnce。warning({ message: ‘請求超時’ }) return Promise。reject(error) })// 是否可以重新整理標識let isRefreshAvailable = true// 快取請求的陣列let requestAttr = []

響應攔截器:

當後端響應token相關錯誤的狀態碼時,10001代表沒有token,10002代表token解析失敗,10003代表refreshToken過期,清空sessionStorage並自動跳轉至登入介面。這裡每有一個請求得到響應,就將loadCount減1,當loadCount為0,且isRefreshAvailable為true時,關閉Loading元件。當後端響應accessToken過期的10000時,根據isRefreshAvailable判斷是否正在重新整理token,isRefreshAvailable為true,表示可以重新整理token,呼叫重新整理token的refreshToken函式,並將isRefreshAvailable設定為false,其他響應不能再呼叫refreshToken函式。為了避免前述的token多次重新整理和請求丟失的兩個問題,重新整理完token,3秒後再將快取陣列中的請求進行重發,並且將isRefreshAvailable設定為true。

Springboot+Axios雙token解決token過期續簽問題

響應攔截器

如果其他token過期的響應回來時正在重新整理token,則使用promise將請求存入快取陣列requestAttr,如果不是token相關的錯誤狀態碼,則列印錯誤結果,如果狀態碼為成功200,則將響應資料返回。

Springboot+Axios雙token解決token過期續簽問題

響應攔截器

http。interceptors。response。use( response => { loadCount—— if (loadCount === 0 && isRefreshAvailable === true) { loadingInstance。close() } if (response。data。status === 10001 || response。data。status === 10002 || response。data。status === 10003) { messageOnce。error({ message: response。data。message + ‘! 3秒後自動跳轉至登入介面’ }) toLogin() } else if (response。data。status === 10000) { if (isRefreshAvailable) { isRefreshAvailable = false refreshToken() // 拿到新accessToken後,等待2-3秒,確保其他請求響應都回來後再重新發送請求 // 防止重發陣列請求後才有請求返回,丟失該部分請求 setTimeout(() => { console。log(‘開始重新發起請求’) requestAttr。forEach((cb) => cb(getAccessToken())) requestAttr = [] isRefreshAvailable = true response。config。headers。Authorization = ‘Bearer’ + getAccessToken() return http(response。config) }, 3000) } else { return new Promise(resolve => { requestAttr。push((token) => { console。log(‘快取陣列的數量:’, requestAttr。length) response。config。headers。Authorization = ‘Bearer’ + token resolve(http(response。config)) }) }) } } else if (response。data。status !== 200) { messageOnce。warning({ message: response。data。message }) } else { return response。data } }, error => { // 對響應錯誤做點什麼 loadCount = 0 loadingInstance。close() messageOnce。error({ message: ‘與伺服器連線發生錯誤’ }) return Promise。resolve(error) })

最終效果,發出7個請求系統時間的請求,得到7個需要重新整理token的響應,只調用了一次refresh介面,又重新發送了7個請求系統時間的請求。

Springboot+Axios雙token解決token過期續簽問題

正確的結果

PS:這個程式碼塊對手機支援不太友好啊,預覽了下,過寬的程式碼塊沒有出現捲軸啊。