承接上篇,建立了一個Cookie-Based
的認證方式
這回要做的是網頁的閒置登出機制
網頁的閒置登出機制可以很簡單、很單純,也可以非常的複雜
比較簡單來說,大概就是登入後放置個幾秒就把你登出
而以下是這次要來挑戰的目標
閒置10分鐘要跳閒置警告
閒置警告
倒數1分鐘後觸發強制登出
閒置警告
可以跳出來2次
當第3次閒置時,觸發強制登出
強制登出
要跳到首頁
要顯示提示訊息,讓user按下確定才會關
閒置登出機制要跨瀏覽器分頁
閒置登出機制要能在手機上作用
完成後的demo如下:
首先,當在桌電上,且單一網頁的情況下,條件1~3其實還算是單純的
直觀上來說,可以做個2層的巢狀Timeout,在配合上user停止動作後去觸發第一層Timeout來進行實作
以下是概略架構:
Timer1 = setTimeout( ()=>{
Timer2 = setTimeout( ()=>{
// 執行強制登出
}, 1 * 60 * 1000 // 倒數1分鐘
}, 10 * 60 * 1000 ) // 倒數10分鐘
接著搭配事件綁定來觸發RunTimer1
每次執行RunTimer1時,都先把Timer1給清掉
這樣就能做到user有動作就重新計算閒置時間的功能
function RunTimer1(){
clearInterval(Timer1)
Timer1.run() // js沒有這個指令,這邊只是示意
}
let unidleEvents = "mousemove click mouseup mousedown keydown keypress keyup submit change mouseenter scroll resize dblclick"
$(window).bind(unidleEvents, RunTimer1)
而以上的事件綁定我們可以用 throttle
來改善效能
如果不進行改善的話,光是滑鼠的移動,就會觸發非常多次的 RunTimer1
而加上 throttle
的話,則可以讓指定的function在一定時間內只被執行一次 [1]
$(window).bind(unidleEvents, throttle(RunTimer1, 200))
多分頁
問題
很顯然的,當多分頁的情況下,光是閒置倒數的時間就會不同
畢竟 RunTimer1 會在不同的時間點被啟動
解法
為了同步跨分頁的Timer,可以利用 localStorage
來進行資料的儲存
並利用 Vue 響應式的機制來觸發主要邏輯
手機
問題
若 User 使用手機進行瀏覽時,若完成登入後,看沒多久,就使用其他APP
那網頁上的Timer,由於手機效能考量的設計,Timer是不會繼續倒數計時的
因此如果現在是倒數50秒跳出,當 User 切換到其他APP使用了100秒後回到網頁
那網頁上的時間還是會在50秒
解法
將 setTimeout
改成 setInterval
並且當 User 開始閒置時,設置一個初始時間點
讓 interval 不斷地用現在的時間跟初始時間點
做計算
這樣當 User 從其他 app 切回來時,時間的計算就會對上了
極端一點來說
若在電腦上剛完成登入就關掉分頁,一個小時候重新開啟這個分頁,那麼理論上應該要直接登出
但如果用 setTimeout
的方式,將可能會變成僅觸發 閒置警告
,或者還在偵測閒置時間,連閒置警告
都不會跳出
綜合以上,若以 Baseline 中,巢狀 Timeout 為基礎,用 setInterval
來進行改造:
let startTimestamp = new Date()
Timer1 = setInterval( ()=>{
if(new Date() - startTimestamp < 10 * 60 * 1000 ){ // 倒數10分鐘
return
}
clearInterval(Timer1)
startTimestamp = new Date()
Timer2 = setInterval( ()=>{
if(new Date() - startTimestamp < 1 * 60 * 1000 ){ // 倒數1分鐘
return
}
clearInterval(Timer2)
// 執行強制登出
}, 200
}, 200 )
最明顯的地方在於,沒有使用 localStorage
因此時間變數還是沒辦法跨分頁共享
再來還有一個問題,當網頁進入閒置警告
時,若時間變數已經從 localStorage
中得到同步
這時如果開新的分頁進入網頁,那新的網頁會優先進入 Timer1 這時如果內部的邏輯沒處理好
將可能導致新的分頁不會進入閒置警告
的畫面
其實以上的Approach遇到很多狀況都需要作出很複雜的處理
比方說登出時Timer要停掉,登入時Timer要啟動,多分頁資料的同步也是個大學問
因此為了讓各種狀況變得比較好應對,我採用響應式的方式來進行實作,基本理念如下
將 Vuex store 改成儲存在 localStorage
這樣就可能在開發時正常操作 store,然後利用 store.commit 來同步資料到 localStorage
將閒置次數區分成 local 與 golbal(存在localStorage)
由於閒置3次要強制登出
,那就必須要有個變數紀錄目前閒置幾次
如果每次觸發閒置警告
,每個分頁都幫 localStorage 中的閒置次數
+1
那有幾個分頁就會增加幾次,那樣次數就會亂掉了
因此,在閒置次數的判斷上,可以用 local 的閒置次數進行判斷
當網頁第一次載入時,從localStorage將值assign到local閒置次數中
當確定要增加次數時,先幫自己+1,接著用 assign 的方式塞到 localStorage
這樣不管assign幾次,localStorage中的值都不會被重複增加
設定閒置狀態
enum IdleDetermineStates{
ByPass, // 不須使用閒置登出狀態
PageIdle, // 要觀察網頁是否閒置
UserComfirm, // 要進行閒置警告的倒數
Logout, // 執行登出的動作
}
設置一個 Interval
使用一個 Interval ,在網頁載入後就啟動不停止
並根據現在的閒置狀態來進行時間的計算,以及狀態的轉移
const intervalRunner = setInterval(()=>{
switch (store.state.pageIdle.idleDetermineStates) {
case IdleDetermineStates.PageIdle:
passSeconds = 閒置秒數
if ( passSeconds >= idleSeconds) {
SetIdleDetermineStates(IdleDetermineStates.UserComfirm)
}
break
case IdleDetermineStates.UserComfirm:
remainUserComfirmSeconds = 倒數計時秒數
if ( remainUserComfirmSeconds <= 0 ) {
SetIdleDetermineStates(IdleDetermineStates.Logout)
}
break
case IdleDetermineStates.Logout:
SetIdleDetermineStates(IdleDetermineStates.ByPass)
break
case IdleDetermineStates.ByPass:
default:
break
}
},250)
設置 watcher 以應對閒置狀態的改變
watch( () => store.state.pageIdle.idleDetermineStates,
(newStates: IdleDetermineStates, oldStates: IdleDetermineStates) =>{
if (oldStates == IdleDetermineStates.PageIdle &&
newStates == IdleDetermineStates.UserComfirm) {
AddIdleTimes()
if ( idleRemainTimes <= 0 ) {
SetIdleDetermineStates(IdleDetermineStates.Logout)
}else{
SetIsShowUserConfirm(true)
}
}
if (oldStates == IdleDetermineStates.UserComfirm &&
newStates == IdleDetermineStates.Logout) {
Logout()
SetIsShowLogOutNotification(true)
}
})
Assign 計算時間點
當 User 有在使用網頁時,就同步紀錄閒置時間的起始時間點 & 閒置警告的起始時間點
function SetTimeStamp(){
const startIdleTimestamp = new Date()
const startUserConfirmTimestamp = new Date (startIdleTimestamp.getTime() + props.idleSeconds * 1000 + 250)
store.commit("pageIdle/SetStartIdleTimestamp", startIdleTimestamp)
store.commit("pageIdle/SetStartUserConfirmTimestamp", startUserConfirmTimestamp)
}
function PageIdleHandler(){
if( store.state.pageIdle.idleDetermineStates == IdleDetermineStates.PageIdle){
SetTimeStamp()
}
}
$(window).bind(unidleEvents, throttle(PageIdleHandler, 200) )
為了達成以上的需求,以下是這次所安裝的套件
npm install -D vuex-multi-tab-state
透過在 plugins 中進行註冊,就能直接讓 store 裡的資料儲存在 localStorage 中
import { createStore } from 'vuex'
import createMultiTabState from 'vuex-multi-tab-state'
export default createStore({
state: {
},
mutations: {
},
actions: {
},
modules: {
},
plugins: [
createMultiTabState(),
],
})
但在使用上有幾個地方要小心
型別
由於 localStorage 中只能儲存字串,因此在拿取資料時,比方說 Date Type 的資料時,要進行轉型一下會比較保險
更新機制
有時為了方便,會使用 store.state.variable = something
的方式來異動資料
但這樣的寫法會無法使 vuex-multi-tab-state
同步更新 localStorage
必須使用 mutation 的方式,套件才能將資料進行同步
npm install -D jquery
npm install -D @types/jquery // 由於有使用到 typescript ,因此要再安裝他的一些定義
npm install -D lodash.debounce
npm install -D @types/lodash.debounce
npm install -D lodash.throttle
npm install -D @types/lodash.throttle
用來進行登出時,右上跳出通知的處理
npm install -D vue-toastification@next
npm install -D material-design-icons
npm install -D animate.css
如果前端 User 一直使用,卻剛好都沒有呼叫到後端的功能的話,後端是有可能自己閒置登出的
因此可以設置一個 KeepAlive()
確定 User 有在動作後,每隔30秒就呼叫一次後端,來避免後端自己閒置登出
後端部分
/// <summary>
/// for refreshing session
/// </summary>
/// <returns></returns>
[HttpPost]
public ActionResult KeepAlive()
{
return Ok();
}
前端部分
$(window).bind(unidleEvents , debounce(KeepAlive, 30 * 1000));
關於 debounce 可以參考 [1:1]
雖然需求的描述只有少少的幾行,跟5個列點,但實際上,第4跟第5點一加下去
WOW,要測的edge case就不少了,這邊就先簡單條列一些狀況:
由於資料會異步更新,因此非常的考驗開發時頭腦的清晰度,否則狀態很容易就弄錯了
再來,開發時能避免巢狀還是儘量避免得好,對腦細胞來說比較友善一點(?
雖然處理多工的難度比較大,但實作出來後的成就感也是滿大的
以上就是一個小小的閒置登出機制,完整的Code在此 歡迎參考看看啦