一. 前言
1.關於JWT的Token過期問題,到底設置多久過期?
(1).有的人設置過期時間很長,比如一個月,甚至更長,等到過期了退回登錄頁面,重新登錄重新獲取token,期間登錄的時候也是重新獲取token,然后過期時間又重置為了1個月。這樣一旦token被人截取,就可能被人長期使用,如果你想禁止,只能修改token頒發的密鑰,這樣就會導致所有token都失效,顯然不太可取。
(2).有的人設置比較短,比如10分鍾,在使用過程中,一旦過期也是退回登錄頁面,這樣就可能使用過程中經常退回登錄頁面,體驗很不好。
2. 這里介紹一種比較主流的解決方案---雙Token機制
(1).訪問令牌:accessToken,訪問接口是需要攜帶的,也就是我們之前一直使用的那個,過期時間一般設置比較短,根據實際項目分析,比如:10分鍾
(2).刷新令牌:refreshToken,當accessToken過期后,用於獲取新的accessToken的時候使用,過期時間一般設置的比較長,比如:7天
3.獲取新的accessToken的時候, 為什么還需要傳入舊accessToken,只傳入refreshToken不行么?
仔細看下面的解決思路,只傳入refreshToken也可以,但是傳入雙Token安全性更高一些。
二. 解決方案
1. 登錄請求過來,將userId和userAccount存到payLoad中,設置不同的過期時間,分別生成accessToken和refreshToken,二者的區別密鑰不一樣,過期時間不一樣,然后把 生成refreshToken的相關信息存到對應的表中【id,userId,token,expire】,一個用戶對應一條記錄(也可以存到Redis中,這里為了測試,存在一個全局變量中), 每次登錄的時候,添加或者更新記錄,最后將雙Token返回給前端,前端存到LocalStorage中。
2. 前端訪問GetMsg獲取信息接口,表頭需要攜帶accessToken,服務器端通過JwtCheck2過濾器進行校驗,驗證通過則正常訪問,如果不通過返回401和不通過的原因,前端在Error中進行獲取,這里區分造成401的原因。
1 //獲取信息接口 2 function GetMsg() { 3 var accessToken = window.localStorage.getItem("accessToken"); 4 $.ajax({ 5 url: "/Home/GetMsg", 6 type: "Post", 7 data: {}, 8 datatype: "json", 9 beforeSend: function (xhr) { 10 xhr.setRequestHeader("Authorization", "Bearer " + accessToken); 11 }, 12 success: function (data) { 13 if (data.status == "ok") { 14 alert(data.msg); 15 } else { 16 alert(data.msg); 17 } 18 }, 19 //當安全校驗未通過的時候進入這里 20 error: function (xhr) { 21 if (xhr.status == 401) { 22 var errorMsg = xhr.responseText; 23 console.log(errorMsg); 24 //alert(errorMsg); 25 if (errorMsg == "expired") { 26 //表示過期,需要自動刷新 27 GetTokenAgain(GetMsg); 28 } else { 29 //表示是非法請求,給出提示,可以直接退回登錄頁 30 alert("非法請求"); 31 } 32 } 33 } 34 }); 35 }
3. 如果是表頭為空、校驗錯誤等等,則直接提示請求非法,返回登錄頁。
4. 如果捕獲的是expired即過期,則調用GetTokenAgain(func)方法,即重新獲取accessToken和refreshToken,這里func代表傳遞進來一個方法名,以便調用成功后重新調用原方法,實現無縫刷新; 向服務器端傳遞 雙Token, 服務器端的驗證邏輯如下:
(1). 先通過純代碼校驗refreshToken的物理合法性,如果非法,前端直接報錯,返回到登錄頁面。
(2). 從accessToken中解析出來userId等其它數據(即使accessToken已經過期,依舊可以解析出來)
(3). 拿着userId、refreshToken、當前時間去RefreshToken表中查數據,如果查不到,直接返回前端保存,返回到登錄頁面。
(4). 如果能查到,重新生成 accessToken和refreshToken,並寫入RefreshToken表
(5). 向前端返回雙token,前端進行覆蓋存儲,然后自動調用原方法,攜帶新的accessToken,進行訪問,從而實現無縫刷新token的問題。
1 //重新獲取訪問令牌和刷新令牌 2 function GetTokenAgain(func) { 3 var model = { 4 accessToken: window.localStorage.getItem("accessToken"), 5 refreshToken: window.localStorage.getItem("refreshToken") 6 }; 7 $.ajax({ 8 url: '/Home/UpdateAccessToken', 9 type: "POST", 10 dataType: "json", 11 data: model, 12 success: function (data) { 13 if (data.status == "error") { 14 debugger; 15 // 表示重新獲取令牌失敗,可以退回登錄頁 16 alert("重新獲取令牌失敗"); 17 18 } else { 19 window.localStorage.setItem("accessToken", data.data.accessToken); 20 window.localStorage.setItem("refreshToken", data.data.refreshToken); 21 func(); 22 } 23 } 24 });
PS:以上方案,適用於單個頁面發送單個ajax請求,如果是多個請求,有順序的發送,比如第一個發送完,然后再發送第二個,這種場景是沒問題的。
但是,特殊情況如果一個頁面多個ajax並行的過來了,如果其中有一個accessToken過期了,那么它會走更新token的機制,這時候refreshToken和accessToken都更新了(數據庫中refreshToken也更新了),會導致剛才同時進來的其它ajax的refreshToken驗證不過,從而無法刷新雙token。
針對這種特殊情況,作為取舍,更新accessToken的方法中,不更新refreshToken, 那么refreshToken過期,本來也是要進入 登錄頁的,所以針對這類情況,這種取舍也無可厚非。