JWT登入鑑權:避免在使用者操作的過程中JWT到期跳轉登入

MarsTokio發表於2021-10-02

當前專案JWT登入鑑權方法及問題

當前登陸鑑權方法

使用者登入時,返回一個JWT,由前端儲存在本地。此後使用者每次向需要許可權的API發出請求時帶上該JWT用於驗證身份。
後端收到JWT後,驗證JWT是否正確且未過期:

  1. 如果正確且未過期,則返回資源。
  2. 如果不正確,則返回不正確的狀態碼;如果已過期,則返回已過期的狀態碼,要求重新登入。

問題

使用JWT實現登入鑑權時,由於JWT有固定的過期時間,可能在使用者正在進行操作的過程中恰好過期,然後突然跳轉到登入介面,造成較差的使用者體驗。

目標

使用JWT實現登入鑑權,當且僅當使用者在一段時間內不對系統進行任何操作時才要求使用者重新登入,並且儘可能保證JWT的安全性。

解決方法及優缺點分析

解決方法一:只使用一個JWT token(不推薦)

具體實現

使用者登入時,返回一個JWT,由前端儲存在本地。此後使用者每次向需要許可權的API發出請求時帶上該JWT用於驗證身份。
後端收到JWT後,驗證該JWT是否正確:

  1. 如果不正確,則直接返回錯誤程式碼;
  2. 如果正確,則驗證該JWT是否過期:

    • 如果沒有過期,則直接返回資源;
    • 如果過期,則計算過期時間:

      • 如果過期時間沒有超過某閾值,則返回資源和新的JWT;
      • 如果過期時間超過某閾值,則返回錯誤程式碼。要求重新登入。
優點

實現了當且僅當使用者在一段時間內不對系統進行任何操作時才要求使用者重新登入。
當JWT過期時如果使用者還在作業系統,會向後端傳送剛剛過期的JWT,此時JWT的過期時間沒有超過閾值,直接返回資源和新的JWT,實現使用者無感書最新。
但是如果當JWT過期之後的一段時間(閾值內)使用者都沒有作業系統,當使用者再次作業系統時,會向後端傳送過期時間超過閾值的JWT,後端會返回錯誤程式碼,實現重新登入。

缺點

安全性較低。如果頻繁傳送請求,可以使用一個JWT實現永久登入。一旦JWT被竊取,攻擊者可以使用得到的JWT永久偽造使用者獲取資訊。

解決方案二:使用兩個JWT token(推薦)

具體實現

使用者登入時,返回兩個JWT(access token和refresh token),一個用於在請求資源驗證身份的access token,一個用於在access token過期時更新access token的refresh token。其中refresh token的有效期較長(如7天),而access token的有效期較短(如1小時)。由前端儲存在本地。此後使用者每次向需要許可權的API發出請求時帶上access token用於驗證身份。
後端收到access token後:驗證該access token是否正確:

  1. 如果不正確,則直接返回錯誤程式碼;
  2. 如果正確,則驗證該access token是否過期:

    • 如果沒過期,則直接返回資源;
    • 如果過期,則返回過期的錯誤程式碼。前端收到過期錯誤程式碼後使用refresh token向更新介面請求新的access token,更新介面判斷refresh token是否過期:

      • 如果沒有過期,則返回新的access token和新的refresh token,將之前所有由同一refresh token生成的refresh token都置為無效(在更新介面維護一個無效列表)。前端使用新的access token重新向需要許可權的API發出請求,獲取資源。
      • 如果過期,則返回錯誤程式碼,要求重新登入。
      • 如果收到已被置為無效的refresh token,則將當前所有與無效refresh token為同一使用者的refresh token和access token都置為無效(具體來說是將該使用者的訪問置為無效),直到使用者重新登入。

使用這種方法,既能避免在使用者操作的過程中出現突然跳轉登入(主要解決了兩次操作之間的不一致現象,不會出現一次請求還能正常獲取資源,下一次請求資源就突然跳轉到登入頁面的現象),又能保證較高的安全性。

解釋
Q:為什麼不使用永久有效的refresh token?

A:為了實現更高的安全性。永久有效的refresh token一旦被竊取,攻擊者可以使用該token永久冒充使用者獲取個人資訊。而每次使用refresh token就更換一個新的refresh token的話,可以通過更新介面返回新refresh token的同時將舊refresh token置為無效的做法,避免攻擊者使用竊取的refresh token獲取新的refresh token/access token。

Q:為什麼refresh token獲取新的access token時需要同時更新refresh token?

A:因為如果不更新refresh token的話,就無法避免使用者兩次操作之間的不一致性。可能使用者在refresh token過期前一分鐘還能正常操作,一分鐘後refresh token和access token同時過期,就突然跳轉到登入頁面,造成不友好的使用者體驗。

Q:為什麼要在使用refresh token獲得新的access token和refresh token後將用於請求新token的refresh token置為無效?

A:為了避免之前的refresh token被竊取後導致的重放攻擊(使用之前的refresh token請求新的access token和refresh token)。

Q:為什麼更新介面在收到已置為無效的refresh token後會將所有使用第一個refresh token得到的refresh token都禁止?

A:因為不知道被竊取的是哪個refresh token。有可能攻擊者使用第一個refresh token,並使用該token在更新介面獲取了新的refresh token,此時使用者可能使用舊的refresh token(已被置為無效)請求更新介面。也有可能攻擊者竊取到第一個refresh token後,使用者首先使用該token在更新介面獲取了新的refresh token,此時攻擊者可能使用舊的refresh token(已置為無效)請求更新介面。詳見:auth0-refresh-token-rotation

Q:收到無效refresh token時,如何將所有與無效refresh token為同一使用者的refresh token和access token都置為無效?

A:

  1. 縮短JWT的有效時間,然後在更新介面維護一個使用者列表,禁止所有該使用者的refresh token:但是access token最短也有一個固定的有效期限(5~10min),可能給攻擊者提供5~10min使用該token的時間。
  2. 維護一個禁止訪問的使用者列表,每次校驗JWT時判斷是否是禁止使用者的JWT,直到使用者重新登入:當一個refresh token被禁止時將該訊息廣播到所有有關的伺服器,所有的伺服器維護一個收到無效refresh token的使用者列表,每次接到一個access token或refresh token時,判斷其對應的使用者是否在列表上,如果是就直接返回錯誤狀態碼。這樣在使用者重新登入之前,不能使用尚未過期的access token訪問任何業務介面,也不能使用尚未過期的refresh token獲取新的access token。
  3. 詳見:fusionauth-revoking-jwts

JWT VS Session:登入鑑權如何選

二者都是為了在無狀態的HTTP協議中記錄登入狀態,以避免每次請求都要驗證身份。

Session實現登入鑑權

使用者登入成功後,伺服器生成一個對應的session ID,儲存在(記憶體裡的)檔案或資料庫中,並且放在cookie中返回給瀏覽器。登入後每次請求會傳送cookie,在服務端的儲存中查詢cookie中的session ID以驗證使用者身份。

對比

  1. (水平)擴充套件性:當使用者量增大時,需要使用多臺伺服器處理訪問。

    • 如果使用session,需要解決伺服器端儲存session ID的問題,因此需要一個集中的session儲存器,一般是一臺專門用於執行redis的伺服器。但是這樣也會面臨訪問量瓶頸的問題。並且同時線上人數過多時需要儲存大量session ID,從而佔用大量伺服器資源。
    • 如果使用JWT,就不用在服務端儲存每個使用者的登入資訊(除了還未過期但被禁用的refresh token,但是不用在使用者請求業務介面時驗證,只用在更新token時驗證,不會影響業務介面的響應時間)。任何一臺伺服器都能直接通過JWT判斷是否是合法的登入使用者。
  2. 效能:如果JWT中包含了很多資料,則它會遠大於一個session ID,會給HTTP請求帶來額外的開銷。但是session每次都要查詢一次資料庫來驗證使用者身份。
  3. 在服務端廢除一個使用者當前所有有效的身份驗證資訊(如使用者修改密碼後禁止使用原來的資訊訪問業務介面):

    • 如果使用session:比較簡單,唯一的身份驗證資訊是session ID,因此直接從資料庫中刪除session ID即可。
    • 如果使用JWT:比較複雜。需要廢除一個使用者所有尚在有效期內的access token和refresh token。而token一旦簽發,在有效期內都能被伺服器認證,因此需要維護一個被禁用使用者的blacklist,請求業務介面/更新access token都要查詢,如果對應的使用者在該blacklist上,則拒絕返回業務資料/新的access token。
  4. 如何退出登入:

    • 如果使用session:刪除session ID。
    • 如果使用JWT:一般的做法是直接將客戶端儲存(cookie或localstorage)的JWT從客戶端刪除。但是登出後,使用上次登入中尚未過期的JWT仍能訪問業務介面/更新介面。如果要在伺服器端實現真正完全的退出登入,請參考3。

參考

相關文章