后端代碼就不貼了,基於.NET5 webapi+jwt+swagger 實現的Token驗證接口。
本文目的
探討一下雙Token驗證設計思路以及Nuxt前端請求攔截器的封裝方法。
所需插件
項目中使用到了cookie-universal-nuxt
gayhub地址:cookie-universal-nuxt
前言
nuxt請求攔截器的封裝比vue的封裝要復雜一些。
主要難點在於封裝請求攔截的時候要對服務端渲染和客戶端渲染區別對待。
當請求走的是服務端渲染時,token傳遞都是在請求頭中進行的。
當請求走的是客戶端渲染時,token傳遞是直接取本機cookie。
期間遇到的問題
1.當使用服務端渲染時應該使用什么容器來存放Token
2.當代碼執行在服務端時,拿到了刷新成功的Token,如何傳遞給客戶端代碼
3.$cookies.set已經設置成了刷新后的Token,為什么在request里拿到的還是舊的過期的Token
4.當一個頁面不止一個請求,並且同時包含服務端渲染和客戶端渲染的請求時,該如何隊列執行。
5.當頁面不止一個請求,當第一個請求訪問Token過期,並且進入刷新狀態,如何處理后續請求。
最后
封裝nuxt請求攔截器期間遇到了太多坑,無奈之下看了cookie-universal-nuxt和js-cookie的源碼才成此文。
下面是請求攔截器代碼,注釋已經夠詳細了,代碼測試環境和線上環境運行均無BUG。
1 /** 2 * 請求攔截器 3 * 名詞解釋 4 * accessToken:訪問令牌 5 * refreshToken:刷新令牌 6 * 設計方案:用戶登錄成功后服務端返回訪問令牌和刷新令牌,訪問令牌1分鍾過期,刷新令牌1小時過期 7 * 訪問令牌過期后,重新把訪問令牌和刷新令牌傳遞給接口,接口返回新的訪問令牌和刷新令牌 8 * 設計初衷: 9 * 當用戶停留在某個頁面超過一小時未做任何操作,則會強制讓用戶重新登錄 10 * 當用戶一直保持着對網頁進行操作時,則能實現永久不需要退出而刷新Token 11 */ 12 export default ({ $cookies, store, redirect, $axios, req }) => { 13 // 從全局導出類中加載RequestUrl作為$axios的baseURL 14 $axios.defaults.baseURL = store.$GlobalHelper.GlobalRequestUrl 15 $axios.defaults.timeout = 3000 16 // 表示是否正在刷新Token 17 // js中的節流閥,相當於線程鎖 18 // 當正在刷新Token時,后續所有繼續發來的accessToken失效的請求都應該被緩存到數組requestsCache中,直到Token刷新結束再重發請求 19 let isRefreshingTokenLock = false 20 21 // 請求緩存數組 22 // 用來存放正在刷新Token時,后續所有的請求 23 let requestsCache = [] 24 25 // 攔截請求,將accessToken插入到請求頭中 26 $axios.onRequest((config) => { 27 // 這里的get要注意 28 // 如果代碼在服務端執行,則是從req.headers里面拿Cookie 29 // 如果代碼在客戶端執行,則是從本地拿Cookie 30 // 所以在使用這句代碼之前,一定要確保判斷是在服務端還是客戶端執行,並且確保拿到的cookie是最新的 31 const accessToken = $cookies.get('accessToken') 32 accessToken && (config.headers.Authorization = accessToken) 33 }) 34 35 // 攔截返回,當accessToken過期,則利用refreshToken進行刷新,並且緩存所有accessToken失效的請求進行重發 36 $axios.onResponse(async (response) => { 37 // 如果響應的狀態碼不是401,代表accessToken並未失效,則直接將response成功結果返回 38 if (response.data.status !== 401) { return Promise.resolve(response) } 39 40 // 如果正在執行刷新Token操作 41 if (isRefreshingTokenLock === true) { 42 global.console.log('正在刷新Token') 43 // 創建Promise的函數對象 44 const promise = new Promise((resolve, reject) => { 45 requestsCache.push((newCookie) => { 46 resolve($axios(response.config)) 47 }) 48 }) 49 // 直接把異步對象返回,由於該對象還沒有被執行resolve函數,所以調用方使用await等待的時會一直掛起,直到resolve被執行 50 return promise 51 } 52 53 // 401表示請求過期,如果訪問Token過期,則應該使用刷新Token去重新獲取訪問Token和刷新Token 54 if (isRefreshingTokenLock === false) { 55 // 設置節流閥,表示正在執行刷新Token 56 isRefreshingTokenLock = true 57 // 從req請求頭或者本地cookie里獲取Token 58 const accessToken = $cookies.get('accessToken') 59 const refreshToken = $cookies.get('refreshToken') 60 // 如果請求令牌或刷新令牌不存則直接返回 61 if (!accessToken || !refreshToken) { 62 return Promise.resolve(response) 63 } 64 // 調用遠程API刷新Token 65 const object = { accessToken, refreshToken } 66 const apiTokenResult = await $axios.post('/api/Token', object) 67 // 123表示刷新Token失敗,如果刷新失敗,將刷新失敗的結果返回 68 if (apiTokenResult.data.status === 123) { 69 return Promise.resolve(apiTokenResult) 70 } 71 72 // 將返回的新cookie保存到客戶端 73 // 因為如果不是ssr,$cookies.get是從本機拿cookie 74 $cookies.set('accessToken', apiTokenResult.data.response.accessToken) 75 $cookies.set('refreshToken', apiTokenResult.data.response.refreshToken) 76 77 if (process.server) { 78 // 將返回的新的cookie保存到req.headers 79 // 因為如果是ssr,$cookies.get是從req.headers拿cookie 80 const newCookie = 'accessToken=' + apiTokenResult.data.response.accessToken + ';' + 'refreshToken=' + apiTokenResult.data.response.refreshToken 81 req.headers.cookie = newCookie 82 } 83 // 設置節流閥,表示刷新Token執行完成 84 isRefreshingTokenLock = false 85 global.console.log('令牌刷新成功') 86 87 // 下面這一步最重要,這里是循環requestsCache數組,並且調用該數組中的所有函數 88 // 當函數被調用時,就會執行resolve通知Promise執行成功,調用方的await才會繼續執行 89 requestsCache.forEach((value) => { 90 value() 91 }) 92 // 清空緩存 93 requestsCache = [] 94 95 return $axios(response.config) 96 } 97 }) 98 }