緣起
哈嘍大家周一好!不知道小伙伴們有沒有學習呀,近來發現各種俱樂部搞起來了,啥時候群里小伙伴也搞一次分享會吧,好歹也是半千了(時間真快,還記得5個月前只有20多人),之前在上個公司,雖然也參與組織過幾次活動,這個再說吧,畢竟都是五湖四海的小伙伴,不太好聚😂。今天要說的內容很簡單,但是個人感覺很實用,從文章標題就可見一斑:JWT的滑動授權,這個問題我被問了不下 n 次,從 6 個月前開始第一次寫 JWT 授權,就有小伙伴陸陸續續在群里提問,說如何然這種無序化的 Token 令牌(不像 Session 那樣,一直存在會話狀態),達到滑動刷新,實現用戶的無感知授權,我也一直在思考,大抵有以下一些思路:
1、token 失效后,直接跳轉到登錄頁; // UE體驗感賊差
2、將 JWT 、 用戶標識 ( 如:id ) 、過期時間等令牌信息存到數據庫,配合用戶進行操作; // 額外的操作太多,連接數據庫
3、同樣的上邊的這些信息放到 Redis 里,再配合緩存,也可以高效處理;// 雖然不操作數據庫,但是變相破壞Token的無序性
4、返回前端兩個token,通過 refresh_token 來刷新 access_token;// 本文要說明的,和這個類似的策略方法
5、在后端處理,Id4中,自帶了 RefreshToken,自動可以重新獲取token;// 這個在下一個系列說到 Id4 的時候,會說到;
這些方法和策略我也一直和群里小伙伴討論,但是卻一直沒有寫文章,也一直沒有真正的通過代碼寫出來,以前偷懶是因為只有后台 .net core 項目,后來自己又偷懶說只有博客項目,直觀上不好實現,現在好了,終於在這段時間上線了后台管理系統,終於把這個問題提上了日程,那下邊就開始今天的說明吧。
老規矩,還是先看效果(這篇文章比較簡單,但是有一丟丟的繞,希望看的時候,可以有十多分鍾的安靜時間,不要着急,自己研究出來的永遠比問出來的要高效的多):
故事背景:
當前 Token 將於 18:05:14 失效,以前的情況是,在失效后,直接跳轉到登錄頁,但是現在不是了,
在 18:05:19 的時候,執行查詢,我們重新對 Token 進行無縫刷新,然后自動重發請求並成功加載數據,是不是達到了你想要的目的?
老張說,這只是對JWT使用者簡單處理,如果高並發,或者大數據,更安全驗證,還是建議使用ID4,如果小公司自己用,目前這個就夠了。如果你有很多顧慮和疑問,請看下邊的評論席,肯定會有小伙伴和你有類似的心情,歡迎批評指正,最后:想要更好的授權需求,還是用Id4,JWT只不過是練手。
那這個到底是如何實現的呢,復雜不復雜呢?如果是你想要的,請往下看 👍,保證每個人都能看懂,前提是你有 JWT 基礎,至少用過。
一、我的設計思路是怎樣的
1、傳統的授權流程
傳統的授權登錄呢,很簡單,也很直白,就是我們平時使用的,因為不像 session 那樣,可以一直保持着狀態,當我們的 Token 失效了以后,就只能重新獲取一個新的 Token 令牌,這不僅僅是它的優點也是一個缺點,
優點就是可以支持分布式,多點式的訪問,session 就不能實現分布式;
缺點當然也是顯而易見,當其過期了,就無法續簽,或者一直保持激活狀態;
我們就只能重新獲取一個,所以一般有的開發者就索性把 Token 的過期時間定的很長,比如一天,一周,甚至十天,只有用戶在當前電腦上登錄一次,以后就可以隨心訪問了,除非自己手動點擊退出登錄,說真的,這種情況我也在使用,因為我們公司的項目有一些是內部的前台項目,比如一個Tool,一個圖表系統,或者一個簡單的個人數據展示,一不怕被外網看到,不會被篡改,二沒有公司其他人來使用我的電腦,我就定義了一個月的失效時間,平時就完全不用登錄了,想想也是可以的。
但是,更多的是需要用戶去實時登錄的,相信大家也用過一直公網的管理后台,關閉瀏覽器或者一段時間不操作以后,就會提示需要我們重新登錄,所以我們就會把 Token 的失效時間定義的很短暫,比如我的一些項目就是 30 分鍾,或者一個小時,這樣不僅更安全,而且也可以應對那些存在變化的,比如后台管理系統,當前用戶的角色變了,總不能還用之前的令牌吧,所以短時的 Token 刷新還是很有必要的。
這樣就會出現一個問題,如何實現滑動授權,就是在流程上,Token還是會失效,但是在用戶體驗 UE 上,實現無感操作,讓用戶在沒有察覺的情況下,實現這個功能,你可以先停下來,想想如何設計,如果想好了,請繼續往下看,看是否和你的思路一致。
2、實現滑動的授權流程
這兩個流程圖對比起來,不同點就在於虛線的問題,由之前的失效即跳轉到登錄頁,多了一個選擇——在用戶活躍期內,通過舊的 token 換取新的 token 繼續體系內循環,這樣就達到了效果(這里還有一種,就是同時發放兩個 token 到前端,一個是access_token,一個是 refresh_token,我做了等價處理,其實這兩種是一樣的)。
這樣不僅能滿足無縫刷新的問題,還能保持 Token 的無序性,那具體的如何在項目中使用呢,請往下繼續看。
二、實現滑動的三步走
從上邊的流程圖中,我們可以看出來,其實要實現滑動刷新很簡單,只需要我們在 Token 失效的時候,重新獲取一個 token,並重新執行一個請求即可,所以我總結了以下三個步驟:
1、定義刷新時間戳
你一定會好奇為什么定義一個刷新時間,不知道你是否還記得上邊我剛剛說到了,其實一般的做法是:每次登錄,向前端丟兩個 token,當我們的 access_token 失效的時候,就判斷 refresh_token 是否有效,如果 refresh_token 有效,我們就把這個 refresh_token 帶到資源服務器,換取新的 access_token,這樣就實現了我們的目的。
但是我們不想這么操作,太麻煩,還需要生成兩個,所以就人為的在前端定義了一個刷新時間點,只要在這個時間點內並且 token 失效了,我就用這個失效的 token 獲取新的token:
在 Login.vue 頁面中定義一個刷新時間:
var token = data.token; _this.$store.commit("saveToken", token);// 保存 token var curTime = new Date(); var expiredate = new Date(curTime.setSeconds(curTime.getSeconds() + data.expires_in)); // 定義過期時間 _this.$store.commit("saveTokenExpire", expiredate); // 保存過期時間 window.localStorage.refreshtime = expiredate; // 保存刷新時間,這里的和過期時間一致
在瀏覽器中查看兩個時間:
2、當執行操作時更新刷新時間(重要)
A:定義方法:
我在 api.js 文件中,定義了保存刷新時間的方法 saveRefreshtime() ,這個的作用主要是記錄當前用戶的操作活躍期,當在這個活躍期內,就可以滑動更新,如果超過了這個時期,就跳轉到登錄頁:
export const saveRefreshtime = params => { let nowtime = new Date(); let lastRefreshtime = window.localStorage.refreshtime ? new Date(window.localStorage.refreshtime) : new Date(-1); let expiretime = new Date(Date.parse(window.localStorage.TokenExpire)) let refreshCount=1;//滑動系數 if (lastRefreshtime >= nowtime) { lastRefreshtime=nowtime>expiretime ? nowtime:expiretime; lastRefreshtime.setMinutes(lastRefreshtime.getMinutes() + refreshCount); window.localStorage.refreshtime = lastRefreshtime; }else { window.localStorage.refreshtime = new Date(-1); } };
上邊的方法中,紅色的是重要的兩點:
1、滑動系數 refreshCount
這個是什么意思呢,就是你自定義的用戶的停止活躍時間段,比如你想用戶最大的休眠時間是20分鍾,說句人話就是,用戶可以最多20分鍾內不進行操作,如果20分鍾后,再操作,就跳轉到登錄頁,如果20分鍾內,繼續操作,那繼續更新時間,休眠時間還是以當前時間+20分鍾。
2、最后刷新時間 lastRefreshtime
這個就是上邊說到的,當用戶操作的時候,實時更新最后的刷新時間,保證用戶活躍時間一直有效,這里有一個重要的就是:
lastRefreshtime=nowtime>expiretime ? nowtime:expiretime;
我為什么要這么寫呢,因為你考慮一下,如果 Token 的過期時間比你自己定義的刷新時間還長,舉個栗子,你后台定義的 token 過期時間是30分鍾,但是你的前端頁面刷新時間是20分鍾,當你登錄后,30分鍾內沒有任何操作,再31分鍾的時候,重新操作,token 肯定是無效了,但是很巧,你的刷新時間也是十分鍾前,那就只能去登錄頁了,這樣達不到刷新的目的,所以我經過大量測試,無論是token過期時間,還是頁面刷新時間,只要取一個較大者就行,然后加上滑動系數,這樣就能滿足各種情況,不信你可以試試。
B:兩處調用:
那現在既然定義了這個刷新方法,在哪里調用呢,我這里想到了兩個地方,當然,你也可以根據自己的需要進行自定義設計,我的是:
1、在路由鈎子里刷新; //在 router.js 的 router.beforeEach 調用方法 saveRefreshtime(),保證每次進行路由切換的時候,都激活用戶活躍時間。 2、在 HttpRequest 鈎子刷新;//在 api.js 的 axios.interceptors.request.use 中調用 saveRefreshtime() ,因為有可能用戶長時間操作同一個頁面,沒有進行路由切換。
我這里就處理了這兩個地方,無論是用戶切換路由,還是在同一個路由的不同按鈕操作,都能保證當前用戶是在操作活躍期的,進而實現滑動的效果。
3、Token無效時,無縫獲取新Token,並重新請求(核心)
現在就到了關鍵時刻了,定義好了刷新時間,那如何進行滑動效果呢?請先看下邊代碼,重點是紅色的部分:
// http response 攔截器 axios.interceptors.response.use( response => { return response; }, error => { if (error.response) { if (error.response.status == 401) { var curTime = new Date() var refreshtime = new Date(Date.parse(window.localStorage.refreshtime))
// 在用戶操作的活躍期內 if (window.localStorage.refreshtime && (curTime <= refreshtime)) {
// 直接將整個請求 return 出去,不然的話,請求會晚於當前請求,無法達到刷新操作 return refreshToken({token: window.localStorage.Token}).then((res) => { if (res.success) { Vue.prototype.$message({ message: 'refreshToken success! loading data...', type: 'success' }); store.commit("saveToken", res.token); var curTime = new Date(); var expiredate = new Date(curTime.setSeconds(curTime.getSeconds() + res.expires_in)); store.commit("saveTokenExpire", expiredate); error.config.__isRetryRequest = true; error.config.headers.Authorization = 'Bearer ' + res.token;
// error.config 包含了當前請求的所有信息 return axios(error.config); } else { // 刷新token失敗 清除token信息並跳轉到登錄頁面 ToLogin() } }); } else { // 返回 401,並且不知用戶操作活躍期內 清除token信息並跳轉到登錄頁面 ToLogin() } } // 403 無權限 if (error.response.status == 403) { Vue.prototype.$message({ message: '失敗!該操作無權限', type: 'error' }); return null; } } return ""; // 返回接口返回的錯誤信息 } );
其中要注意的是三點:
1、判斷是否是在用戶操作活躍期,如果不在,直接跳轉登錄頁,反之,進行 refresh 操作;
2、return refreshToken ,這里是兩個return 的第一個,需要將刷新token的網絡請求返回過去,不然的話,刷新token的請求成功后,當前網絡請求已經結束了,無法達到刷新的目的;
3、return axios(error.config) ,這里就是重新進行一次請求,特別是 error.config ,這個就是我們當前請求的全部信息。
效果如下:
好啦,JWT 滑動授權刷新就到這里已經完成了,是不是很簡單。
三、實現滑動刷新的后端方法
1、Redis,控制 Token 頒發
除了這個前端方法以為,還有后端處理,設計思路也很簡單,我就不多說了,簡單說兩句:
當用戶登錄的時候,生成 access_token ,我們把 token 存在 redis 緩存中,對應匹配用戶標識,狀態等,當用戶修改了密碼,或者當前用戶的權限被超級管理修改的時候,把 redis 中的當前用戶的token 也更新操作,等用戶再次使用的時候,先判斷當前用的 token 是否有效,然后再判斷是否有權限,這樣也能達到效果。如果過期了,還可以把新的token 放到 Header 中返回過去,不過這樣的方法,還是需要配合前端操作,個人感覺還不如上邊的方法。
如果有想嘗試的小伙伴,可以自己嘗試下,我簡單提示一下,就是在后端項目的 PermissionHandler.cs 文件中,對當前 httpContext.Request.Headers["Authorization"] 進行獲取 token 判斷,至於怎么操作這里就不表了。
四、Github && Gitee
https://github.com/anjoy8/Blog.Admin 前端
https://github.com/anjoy8/Blog.Core 后端
-- ♥ -- ♥ -- ♥ -- ♥ -- ♥ -- ♥ --