大部分情況下,React App 會與使用者這樣互動:
當使用者在 App 上做出點擊按鈕、提交表單等動作時,App 會將這些動作轉換成對伺服器的請求。
伺服器收到請求後,會進行驗證與處理,再將結果回傳給 App。
App 接收到伺服器的回應後,更新畫面,將資料呈現出來作為使用者的操作回饋。
使用者、App 以及伺服器的互動示意圖
進一步說,使用者無法直接與伺服器溝通,他必須透過 App 作為中介。App 會把使用者的操作轉換成請求送給伺服器,伺服器回應結果後,App 再更新 UI 呈現給使用者。
但並不是每一筆請求都能成功獲取資料。身為資料的守門人,伺服器不會因為收到了請求就乖乖交出資料。通常,伺服器會想確認請求者是誰、有沒有登入、是否有權限讀取或修改資料,這就是所謂的「驗證與授權」。
為了讓整個流程安全又順利,我們通常會使用 JWT (JSON Web Token)。
JWT 本質上是一段經過簽章的文字,可以儲存使用者的資料。它通常包含三個部分:header、payload 和 signature,其中 payload 裡會放一些非敏感的使用者資訊(像是 userId、email 等),由於這些內容是明碼,不建議放密碼這類敏感資料。
JWT 可以設定有效期限,讓它在一段時間後自動失效,強迫使用者重新驗證。這讓整個驗證機制更安全可控。
在當前的驗證實務中,我們時常會用到兩種 token:
假設一位新使用者第一次使用你的 App,通常會先看到一個註冊或登入的表單。填完後送出,App 會將資料打包成請求送給伺服器。
伺服器收到資料後,會去資料庫查詢:email 是否已存在?密碼是否正確?若一切無誤,就會「簽發」token。
這裡會產生兩個 token:
一個是 refresh token,這個 token 只存在伺服器端,會被存放在 HttpOnly
的 cookie 裡,不能被前端 JavaScript 存取,這樣就算有惡意程式碼植入,也讀不到這個 token,達到第一層安全保護。
另一個是 access token,這是前端 App 需要用的。伺服器會將這個 token 回傳給 App,由 App 保存下來(通常放在記憶體中,而不是 localStorage 或 cookie,原因後面會說)。
之後,使用者在 App 上發出每一個請求(像是讀取個人資料、發文、更新設定等),App 都會附上這個 access token,讓伺服器知道這筆請求是來自哪位使用者。
access token 就像一把鑰匙,但這把鑰匙是由伺服器根據 refresh token 的資訊來簽發的,因此具有驗證身份的效力。
為了安全,存放在前端的 access token 通常會設定很短的有效時間,例如 15 分鐘或 1 小時,因為它是存在記憶體中,若被竊取,損害要設法降到最低。
相反地,refresh token 儲存在 HttpOnly Cookie
中,安全性相對高,可以設得比較久,像是 30 天或 90 天。只要這段期間內 refresh token 沒失效,就可以用它重新獲得新的 access token。
你可能會這樣想:
「我能不能把 token 放在 localStorage?這樣重新整理畫面時 token 還在,多方便!」
但其實這是不推薦的做法,因為 localStorage 是可以被 JavaScript 存取的,一旦網站有 XSS 攻擊漏洞,攻擊者就能撈出你的 token,冒用使用者身份。
因此更安全的做法是:
如果 access token 存在記憶體中,那使用者只要一刷新頁面,就會丟失 token,這時怎麼辦?
別擔心,App 每次啟動時,可以自動發出「refresh token 請求」給伺服器。伺服器會檢查 cookie 裡的 refresh token 是否有效,若是,便簽發新的 access token 給前端,前端再重新設定在記憶體中。
這樣就能讓使用者「維持登入狀態」,卻又不必每次都登入一次。
Access Token 與 Refresh Token 示意圖
HttpOnly Cookie
中。透過這套機制,就能夠兼顧基本的「使用者體驗」與「資訊安全」。