若已連接後端API,使用者會在第一次登入時,拿到 cookie。前端可透過 document.cookie
讀取、寫入甚至刪除 cookie。(cookie 並未加密,仍有安全疑慮) 但因為專案尚未連接登入 API,似乎只能把使用者的登入狀態放在 localStorage 操作。
在 JavaScript 中,透過瀏覽器或Node 等 runtime,可以用 setTimeout
或 setInterval
方法來設置計時器。
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("/");
建立一個 useLogoutIdle
composable,每次呼叫時,會先根據 localStorage 裡面有無 activeUserId
值判斷使用者是否登出,接著做 timeout 的處理:在每次window 發生 blur (失去焦點) 事件時,就設置新的計時器,而若使用者點擊視窗觸發 onMouseDown 事件,則將計時器清除。
完整的 hook 程式碼如下:
為了避免使用者被登出後,畫面突然跳轉而無通知,我需要一個能跳出 toast 通知的套件。在找套件時,發現之前滿受歡迎的 vue-toastificaiton
停止維護了,因此我使用 vue-toast-notification
套件來告知使用者即將被登出。
此次實作還滿能體會 React 和 Vue 原生的 API 設計哲學之差異:在一般的 js 或 ts 檔案中, ref 可以在檔案中的任何位置呼叫,不像 React 的 custom hook 會限定 hook 一定要在元件頂層或是 use-*
裡面使用,這點在開發上帶來較大的彈性。但這也會容易產生盲點,例如沒有在 startTimeout
裡面帶入 timeout
參數,就會沒辦法在呼叫時吃到頂層宣告的 timeout 值,雖然這是 JS 作用域的問題,但把 hook 限制在元件裡或許比較能避免此錯誤?
localStorage 裡的資料,都可以在瀏覽器透過 JavaScript 操作,只靠它去儲存使用者的登入狀態會有問題,較安全的做法,是將使用者重新導向後,再透過後端 API 驗證才較安全。
此外,其實這個 function 並沒有符合 composable 可以在各元件抽取出變數的精神,僅止於在 元件之間共享操作 ref 的邏輯罷了,也許有比 composable 更好的做法。
最後,特別感謝 學.誌|Chris Kang 提供用計時器和 localStorage 實作的想法。原本接到需求時,覺得沒有登入 API 就窒礙難行的我果然想的太狹隘了。