深度主題文

如何設計前端登入機制? 該用 localStorage 還是 cookie 保持登入狀態?

2024年11月11日

💡 E+ 成長計畫深度主題文

本文為 E+ 成長計畫的深度主題文,開放免費閱讀。E+ 的訂閱讀者除了每週會收到一篇新內容外,也享有過去所有內容的閱讀權限。

如果你對於這類深度內容感興趣,歡迎加入 E+ 成長計畫,除了全系列所有完整深度文章外,也可觀看過去所有的直播回放。

在這篇 E+ 前端主題文當中,我們會聊聊從前端的角度,怎麼看註冊登入機制。

所謂的註冊與登入,是從產品、使用者的角度來看,如果從技術的角度看,登入在做的事情其實是驗證 (authentication) 與授權 (authorization)。當一個使用者登入後 (不論透過帳號密碼,或者透過第三方登入、Magic Links 等方式),系統驗證了你的身分,並且根據你有的權限,讓你能做相應的操作。

驗證與授權的差別是什麼?

雖然上面有簡短提到驗證與授權,但沒有很詳細說明兩者的不同,在往下談驗證前,讓我們先定義一下這兩者的差別。

所謂的驗證 (authentication) 做的事情是驗證你的身分,白話來說就是讓系統知道你是你,而不是別人。舉例來說,在登入時輸入帳號密碼,即是一種驗證你確實是你的方式 (假設在安全的狀況下,只有你知道你的帳號密碼,所以可以藉此驗證)。

然而,假如你的密碼設定的不夠安全,且沒有定時更新,導致被被盜用的話,很可能被盜用的人冒充你,騙系統說他是你。這也是為什麼在比較注重資安的系統,通常會有多層的驗證 (例如常見的二階段驗證 2FA,會要求你輸入帳號密碼外,會有像是收簡訊碼等額外個步驟,來驗證你的身份)。

而授權 (authorization) 則是涉及你有什麼權限。在系統確認你確實是你後,系統會需要進一步判斷你有什麼權限,並基於你有的權限,開放讓你能做不同的操作。

以最常見的文件系統來說,在一間公司當中,不同職級的人可能會有不同的權限,例如執行長有最大的權限,所以什麼文件都能閱覽,但一位剛進公司的初階工程師的權限有限,會有部分文件沒辦法閱覽。

在了解完驗證與授權的差別後,讓我們接著來看有什麼常見的驗證方式。

常見的驗證方式

當談到驗證時,最直觀會想到的是透過帳號密碼來驗證。在理想的狀況下,一個使用者的帳號密碼,只有使用者自己知道,所以透過帳號密碼可以確認該使用者是本人。

然而,現實的狀況是,很多使用者的密碼會用好猜的常見密碼,或者沒有保管好密碼,讓密碼的方式不太有保障。因此現在業界普遍的做法,會是搭配上面有提到的二階段驗證 (2FA),例如透過收簡訊、Email 等方式再次驗證;另外有些比較嚴格的系統,甚至會做到多重要素的驗證 (MFA),就是指用兩種以上的方式來驗證。

除了帳號密碼外,另一種不透過帳號密碼的驗證方式,是無密碼驗證 (passwordless authentication),常見的是透過 Magic Links 的方式,讓使用者輸入信箱,然後伺服器寄送信件到使用者信箱,使用者點及信件中的 Magic Links 來進行登入。或者透過 OTP (one-time password),由系統在每次要登入前,傳送這個一次性的密碼到使用者的裝置,然後用該 OTP 登入進行驗證,而不是由使用者自己設定的密碼來驗證。除此之外,指紋、瞳孔等生物特徵,也都算是無密碼的驗證方式。

當然,提到驗證,現在幾乎多數的應用程式都會有第三方登入,例如讓你直接用諸如 Google、GitHub 等不同第三方直接登入。常見的第三方登入是透過 OAuth 來做到。

總的來說,在目前的業界中,上面談到的這幾種驗證方式,都是很常見的

  • 使用者輸入密碼
  • 二階段驗證 (2FA)、多重要素驗證 (MFA)
  • 無密碼驗證
  • 第三方登入

從前端的角度看驗證

從前端的角度看驗證機制的設計,除了「用什麼方式驗證外」,一個前端工程師都需要思考的是「目前在開發的應用程式,是用什麼方式渲染」,因為客戶端渲染 (CSR) 跟伺服器端渲染 (SSR) 會有一點不同,這讓在思考驗證時切入的角度也會不同。

先看客戶端渲染 (CSR),客戶端渲染的驗證,一般是根據驗證與否,來決定是否展示某些頁面、功能;沒有驗證的使用者,會被轉導到登入註冊頁面。因此,一般來說,會是把登入與否的檢查,放在客戶端最外層。然後會在頁面中檢查,如果有登入態,才能看到某些頁面。

然而,如果是伺服器端渲染,因為有了伺服器這個元素在,驗證的部分可以不用在客戶端做檢查,而是可以提前一步在中間件 (middleware) 層做檢查。具體來說,當使用者要訪問某個頁面時,最開始請求發送後,會先在中間件查看是否已登入,如果沒有的話,甚至不會渲染該頁面,而是直接轉導到登入註冊頁。

session-based 還是 token-based?

在談驗證,一般前端會區分成 session-based 與 token-based 兩種方式。如果要抽象化一個層次來看,這兩種方式分別會是有狀態 (stateful) 以及無狀態 (stateless),而這兩種取向各有優缺。

有狀態的驗證方式,伺服器端會創建 session,然後客戶端會記下 session id,每次請求時帶上 session id (一般會透過 cookie 帶上),然後伺服器端收到後檢查這個 session 是否還是有效狀態,如果是的話就完成驗證。

一般來說,有狀態的驗證會比較安全,因為伺服器端可以管理狀態,所以如果被駭客攻擊,伺服器端可以主動把 session 設成無效,來避免被攻擊。

而無狀態的驗證,一般透過 token 來做到,透過加密演算法生成的 token,可以直接驗證 (例如透過 JWT,可以像下方的圖這樣直接 decode)

JWT decode
JWT decode
圖片來源:https://jwt.io/%E3%80%82

無狀態驗證的好處是,速度會快非常多。以一般 token 的 decode 來說,大約 1 毫秒就完成;但是查看 session 會需要去資料庫撈資料來比對,通常至少 20 - 30 毫秒。當量大起來,兩者在耗時的差異會變很明顯。

回到原本的問題,目前業界主流會採用混合作法 (hybrid approach),也就是無狀態搭配有狀態,讓兩者的優點都能兼顧到。具體的作法,是現在常見的 access token 搭配 refresh token 的方式。讓 access token 是無狀態,所以能維持速度快的優勢;同時設定短過期時間 (例如每分鐘或每小時過期),過期後用 refresh token 去換新的 access token。由於 refresh token 的過期時間會設定比較長,使用者不會因為 access token 過期就要重新登入,是直到 refresh token 過期才要重新登入。

拿 refresh token 去換新的 access token 這件事情是有狀態的,因此後端就可以去控制,當遇到安全性問題 (例如被攻擊),就可以把 token 變無效。這種混合的搭配方式,就能維持快速又安全。

localStorage 還是 cookie?

從前端的角度看,如果要避免每次驗證時,使用者都要重新輸入帳號密碼來,一般會把登入態存下來。如果是 session-based 的驗證,會把 session id 存在瀏覽器的 cookie;而選擇 token-based 的驗證 (例如 JWT) 則可以把 token 存在 localStorage 也可以存在 cookie

這時可能會進一步問的問題是,如果兩邊都能存,該存哪邊?

目前社群中主要支持 cookie 的觀點,包含以下:

  • 從安全的角度來看,目前社群的觀點是 cookie 會比 localStorage 來的安全一點 (見此討論的第一個回答)。最主要的原因在於,如果網站被 XSS 攻擊成功,存在 localStorage 的 token 是可以被偷走的;但是 cookie 若有帶上 HttpOnly 就可以避免在 XSS 時被偷到。從這個角度來看會比較安全一點。
  • 如果元件的渲染不是發生在客戶端,那就沒有辦法用 localStorage。舉例來說,目前主流前端框架越來越往伺服器端靠齊,以 React 的 Server Component 來說,因為是在伺服器端的元件,所以在伺服器端時沒辦法操作 localStorage,但是仍可以收到請求發送時夾帶的 cookie。因此這種狀況,只能使用 cookie

但也有支持非 cookie 的觀點:

  • 首先,用 cookie 的問題在於,如果在非瀏覽器環境,就沒有 cookie,所以如果同時要開發行動裝置的應用程式 (mobile app),在前端選 cookie 意味著在行動端要有另一套方案,這會是額外的開發成本。
  • 另外,關於安全性的問題,防止 XSS 等攻擊,不該是驗證端的責任,而是在開發的其他細節要確保好;如果能做好 XSS 的防禦,就無須擔心 localStorage 被偷的問題。

雖然有不同觀點,但如果從前端的角度來看,目前還是偏好存在 cookie。承上一段落的討論,目前普遍的做法,是會把有效期限短的 JWT 存在瀏覽器能拿到的 cookie 當中,而把用來 refresh 的 token 存在 HttpOnlycookie 中。

這樣一來,就算遇到 XSS 攻擊,有效期短的 JWT,因為通常存活設定是 60 秒,時間太短也沒辦法真的被拿去做什麼;而放在 HttpOnlycookie 的不擔心 XSS,這樣搭配下來能做到前一段提到的快速又安全。

💡 access token 也放在 `HttpOnly` 的 cookie 會比較安全嗎?

讀到上面這段,你可能會問,access token 是不是也放在 HttpOnly 的 cookie 會比較安全?

目前確實是有這種觀點,認為 access token 也該放在 HttpOnly 的 cookie,例如 Auth.js (前身是 NextAuth) 就是把兩種都放在 HttpOnly的 cookie

這段業界專家的討論中 有提到 Keep access tokens in private memory or apply same protection means as for refresh tokens ,意即把 access token 放在跟 refresh token 同樣安全的地方,是防禦方式之一。

但選擇放一般 cookie 的論點是,在 access token 這段的主要防禦是來自有效期間短 (Keep access token lifetime short ) 這點,所以著重點會是這個,要不要放在 HttpOnly 的 cookie 不是這段的重點。

無密碼驗證 (passwordless authentication) 的一大好處之一,在於因為沒有密碼,所以不用擔心使用者設定很容易被駭的密碼,或者不用擔心使用者忘記密碼的問題。這邊我們特別拉出常見的無密碼驗證 Magic Links 來討論。

Magic Links 的驗證方式如下:

  • 當使用者到登入頁面時,要輸入信箱,這時後端會產出 Magic Link,然後透過寄送該 Magic Links 到使用者的信箱。使用者點擊 Magic Link 後,會轉導到網站。
  • Magic Link 一般有兩個元素組成,一個是 url 另一個是 token 的方式,例如 https://explainthis.io/authenticate?token=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 這個 url 一般會設定成驗證的頁面,當轉導到該頁面時,會帶上 token 來發送請求給登入用的 API,藉此完成驗證。
  • 在完成驗證後,伺服器端會建立 session,然後透過 cookie 等方式,讓前端可以在發送請求時帶著 (一般會透過 HttpOnlySecure 等設定來確保安全性),而這之後基本上就跟用帳號密碼登入的形式相同。
Magic Links 的驗證方式流程
Magic Links 的驗證方式流程

從安全性角度來看,Magic Link 上帶的 token,應該要符合幾個要件

  • 有過期時間限制:通常會設定幾分鐘後過期,這樣外洩或被盜時如果過期了,就仍可以避免被盜
  • 有使用次數限制:通常會設定一次,讓該 Magic Link 是一次性的,同樣出於安全考量,避免外洩時被重複使用
  • 可撤銷性:系統要能主動撤銷該 token,這樣被盜也可以由系統直接撤銷

雖說 Magic Links 或者 OTP 這類無密碼驗證,能免去有密碼會遇到的問題,但也是有其不那麼理想的地方。舉例來說,使用者要額外去收信,這個額外操作在使用者體驗上可能會讓使用者覺得麻煩;又或者因為有額外寄信的動作,等於有額外的伺服器要照顧 (可能有網路傳輸、安全等問題要處理)

第三方驗證之 OAuth

第三方驗證也是近年來多數應用程式會加入的註冊登入方式,透過第三方驗證,可以讓使用者在註冊時的阻力大幅減少。最常見的是 Google 登入,因為多數人早就已經有 Google 的帳號,所以註冊時直接用,點幾個鍵就完成,不用再重新填寫註冊時的相關資訊。

目前主要實作第三方驗證的方式,會是透過 OAuth 2.0 來完成。OAuth 的全稱是 Open Authorization,也就是開放授權的意思。開放授權在解決的問題,是讓你能在某個應用程式被授權,進而能訪問到在另一個應用服務 (第三方的服務) 的某些資源。

可以授權的資源有很多,舉例來說,現在很多便利商店的雲端列印,透過授權讀取你上傳到 Google Drive 裡面的文件,然後直接列印那些文件。又或者,可以透過 Stripe 等支付平台的 OAuth 來完成某些金流的操作。

而透過 OAuth 的註冊登入,則大致會透過以下流程完成 (以 Google 註冊/登入為例)

  • 使用者到 ABC 網站
  • 使用者點擊 Google 註冊
  • 使用者被導向 Google 的授權頁面
    • 如果是未登入 Google 帳號的狀態,則會需要先登入
    • 該頁面會列出要授權獲取的資訊,例如 To continue, Google will share your name, email address, language preference, and profile picture with ABC
  • 使用者點擊同意授權
  • Google 會把使用者轉導到指定的頁面 (該頁面會是由 ABC 網站在設定 Google OAuth 時設定的),同時帶上 auth code 來換取 access token
  • ABC 網站透過該 token 去從 Google 取得上述的姓名、信箱等資訊
  • ABC 網站用取得的資訊來註冊新帳號,然後把該 Google 帳號與新創建的帳號關聯在一起
  • 之後用戶點擊 Google 登入,就直接可以用關聯到的帳號
OAuth 的註冊登入流程
OAuth 的註冊登入流程

關於 OAuth 的進階討論,多半會與安全相關,舉例來說常見的中間人攻擊,或者 重新導向的 url 被篡改等。但礙於篇幅,這篇先不會討論到,有興趣的讀者,可以在 E+ Discord 討論區中進一步討論。

小結

以上介紹了前端登入機制的主要考量,並且針對無密碼驗證、第三方驗證都做了相關介紹,如果對於前端登入機制有問題。

💡 E+ 成長計畫深度主題文

本文為 E+ 成長計畫的深度主題文,開放免費閱讀。E+ 的訂閱讀者除了每週會收到一篇新內容外,也享有過去所有內容的閱讀權限。

如果你對於這類深度內容感興趣,歡迎加入 E+ 成長計畫,除了全系列所有完整深度文章外,也可觀看過去所有的直播回放。