從壹開始前后端 [vue后台] 之二 || 完美實現 JWT 滑動授權刷新


緣起

哈嘍大家周一好!不知道小伙伴們有沒有學習呀,近來發現各種俱樂部搞起來了,啥時候群里小伙伴也搞一次分享會吧,好歹也是半千了(時間真快,還記得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 后端 

-- ♥ -- ♥ -- ♥ -- ♥ -- ♥ -- ♥ --


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM