更新於 2024/04/28閱讀時間約 5 分鐘

如何強制登出閒置網頁的使用者

大綱

  • 為什麼要登出使用者?
  • 思路
  • 實作過程
  • 心得



為什麼要登出使用者?

  • 防止未經授權的人,在使用者暫離時使用系統,公用或共享電腦的環境中尤其重要。
  • 只要使用者處於登入狀態,就會暴露在個人資料被他人操縱或利用的風險中,因此登出閒置使用者對資安也很重要。


思路

要如何從前端登出使用者?

若已連接後端API,使用者會在第一次登入時,拿到 cookie。前端可透過 document.cookie 讀取、寫入甚至刪除 cookie。(cookie 並未加密,仍有安全疑慮) 但因為專案尚未連接登入 API,似乎只能把使用者的登入狀態放在 localStorage 操作。


該如何設置、清除和重置 timer?

在 JavaScript 中,透過瀏覽器或Node 等 runtime,可以用 setTimeoutsetInterval 方法來設置計時器。

setTimeout 會設置一次性的計時器,並回傳一個數字id,可利用clearTimeout(id)將該計時器清除。 setInterval 則會設置一個持續計時的計時器,除非利用回傳的 id 執行clearInterval(id) ,否則不會停止計時。登出閒置使用者的功能,看似是要持續監聽使用者行為,實際上會需要在使用者閒置和重新點擊視窗時重置計時器,因此我選用 setTimeout


實作過程

專案的頁面會有很多,可以想見需要一個可重複運用在各元件的邏輯,來檢查使用者的閒置時間是否超過上限,因此我選用 Vue 3 Composition API 的 composable 在元件之間共享邏輯。

在登入表單中,會把使用者 id 透過 activeUserId 這個 key 存進 localStorage 中,等下登出時就會做刪除。

function submitForm() {
console.log("Submitting:", email.value, password.value);
isLoggedIn.value = true;
const userId = (currentUserId.value = Math.random());
localStorage.setItem("activeUserId", userId);
}

使用者被登出後,需要將其導向首頁,這個行為由 Vue Router 的 router.replace() 負責。之所以用 replace 而非 push,是要在瀏覽器的 history 物件中,用首頁取代登出前所在的頁面,防止使用者利用瀏覽器的上一頁按鈕回到之前的頁面。

isLoggedOut.value = true;
console.log("User is logged out due to inactivity");
router.replace("/");

Composable

建立一個 useLogoutIdle composable,每次呼叫時,會先根據 localStorage 裡面有無 activeUserId 值判斷使用者是否登出,接著做 timeout 的處理:在每次window 發生 blur (失去焦點) 事件時,就設置新的計時器,而若使用者點擊視窗觸發 onMouseDown 事件,則將計時器清除。

在 hook 裡面宣告 isLoggedOut ref。


完整的 hook 程式碼如下:

Toast

為了避免使用者被登出後,畫面突然跳轉而無通知,我需要一個能跳出 toast 通知的套件。在找套件時,發現之前滿受歡迎的 vue-toastificaiton 停止維護了,因此我使用 vue-toast-notification 套件來告知使用者即將被登出。



vue-toast-notification 樣式簡單好看


心得

此次實作還滿能體會 React 和 Vue 原生的 API 設計哲學之差異:在一般的 js 或 ts 檔案中, ref 可以在檔案中的任何位置呼叫,不像 React 的 custom hook 會限定 hook 一定要在元件頂層或是 use-* 裡面使用,這點在開發上帶來較大的彈性。但這也會容易產生盲點,例如沒有在 startTimeout 裡面帶入 timeout 參數,就會沒辦法在呼叫時吃到頂層宣告的 timeout 值,雖然這是 JS 作用域的問題,但把 hook 限制在元件裡或許比較能避免此錯誤?

直接在檔案頂層使用 ref 很方便。

localStorage 裡的資料,都可以在瀏覽器透過 JavaScript 操作,只靠它去儲存使用者的登入狀態會有問題,較安全的做法,是將使用者重新導向後,再透過後端 API 驗證才較安全。

此外,其實這個 function 並沒有符合 composable 可以在各元件抽取出變數的精神,僅止於在 元件之間共享操作 ref 的邏輯罷了,也許有比 composable 更好的做法。

最後,特別感謝 學.誌|Chris Kang 提供用計時器和 localStorage 實作的想法。原本接到需求時,覺得沒有登入 API 就窒礙難行的我果然想的太狹隘了。

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.