JWT的刷新問題
想對新REST API實施基於JWT的身份驗證,但是令牌中設置了到期時間,感覺使用不太方便,那麽問題來了,是否可以自動延長這個過期時間呢?如果用戶頻繁使用應用程序,就不希望用戶每X分鍾後需要重新登錄。老是要用戶登錄,從用戶體驗的角度來說,肯定是非常糟糕的。
但延長過期時間會創建一個新的令牌(而且同時舊的令牌仍然有效,直到它過期)。並且在每個請求之後生成一個新的令牌,感覺是比較愚蠢的做法。另外,當有多個令牌同時有效時,感覺還會有安全問題。當然,我可以使用黑名單使舊的令牌失效,但是我需要存儲令牌。而JWT的優點之一就是無需存儲空間。
我找到了Auth0如何解決它的方式。他們不僅使用JWT令牌,還會刷新令牌:https://docs.auth0.com/refresh-token
但是還是同樣的問題,為了實現這一點(不使用Auth0),我需要存儲刷新令牌並維護它們的到期時間。那麽這麽做到底有什麽用呢?為什麽不隻有一個令牌(不是JWT)並且在服務器上保持到期?
是否還有其他選擇?是不是JWT不適合這種情況呢?
最佳解決方法
我在Auth0公司工作,我參與了刷新令牌功能的設計。
這一切都取決於應用的類型,下麵是我們推薦的方法。
Web應用程序
一個好的模式是在它過期之前刷新令牌。
將令牌過期時間設置為一周,並在每次用戶打開Web應用程序並每隔一小時刷新令牌。如果用戶超過一周沒有打開過應用程序,那他們就需要再次登錄,這是可接受的Web應用程序UX(用戶體驗)。
要刷新令牌,API需要一個新的端點,它接收一個有效的、沒有過期的JWT、並返回與新的到期字段相同的簽名的JWT。然後Web應用程序會將令牌存儲在某處。
移動/本地應用程序
大多數本地應用程序的登錄有且僅有一次。
這裏麵的出發點是,刷新令牌永遠不會過期,並且可以始終為有效的JWT進行更換。
永遠不會過期的令牌的問題是它失去了令牌的意義。譬如,如果你電話丟了,你該怎麽辦?因此,它需要由用戶以某種方式進行識別,應用程序需要提供撤銷訪問的方法。我們決定使用設備的名稱,例如“maryo的iPad”。然後用戶可以去應用程序,並撤銷訪問“maryo的iPad”。
另一種方法是撤銷特定事件的刷新令牌,其中一個有趣的事件是更改密碼。
我們認為JWT對於這些用例無效,因此我們使用隨機生成的字符串,並將它們存儲在我們這邊。
次佳解決思路
在您自己處理身份驗證(即不要使用Auth0等提供者)的情況下,可能需要執行以下操作:
-
發送JWT令牌的時間相對較短,比如15分鍾。
-
應用程序在需要令牌的任何事務(令牌包含到期日期)之前檢查令牌到期日。如果令牌已過期,那麽它首先要求API刷新令牌。從用戶體驗的角度來說,這是完全透明的。
-
API獲取令牌刷新請求,但首先檢查用戶數據庫以查看是否針對該用戶配置文件設置了’reauth’標誌(令牌可以包含用戶標識)。如果標誌存在,則令牌刷新被拒絕,否則發出新的令牌。
-
重複上述過程。
數據庫後端的’reauth’標誌在例如用戶已經重置密碼時被設置。當用戶下次登錄時,該標誌將被刪除。
此外,假設您有一個策略,用戶必須至少每72小時登錄一次。在這種情況下,您的API令牌刷新邏輯還將從用戶數據庫中檢查用戶的最後登錄日期,並在此基礎上拒絕/允許令牌刷新。
第三種解決思路
在後端使用RESTful APIs將應用程序遷移到HTML5時,我正在做相關的修修補補的工作。我想出的解決辦法是:
-
在成功登錄後,客戶端會發出會話時間為30分鍾(或任何常用的服務器端會話時間)的令牌。
-
創建一個客戶端計時器來調用服務在其到期之前更新令牌。新的令牌將取代以後的調用。
如您所見,這樣可以減少頻繁更新令牌請求。如果用戶在更新令牌調用觸發之前關閉瀏覽器/應用程序,則先前的令牌將及時到期,用戶將必須重新登錄。
可以實現更複雜的策略來滿足用戶不活動時的情形(例如忽略打開的瀏覽器選項卡)。在這種情況下,續訂令牌調用應包括預期到期時間,而這個時間不應超過定義的會話時間。那麽應用程序必須相應地跟蹤上一次用戶的交互。
我不喜歡設置長期到期時間的思路,因為這種方法對於不需要那麽頻繁身份驗證的本機應用程序可能無法正常工作。
其他解決思路
jwt-autorefresh(JWT自動刷新)
如果您正在使用這種節點節點(React /Redux /Universal JS),則可以安裝npm i -S jwt-autorefresh
。
該庫以用戶計算的訪問令牌到期之前的秒數(基於在令牌中編碼的exp索引)來計劃刷新JWT令牌。它有一個廣泛的測試套件,並檢查了很多條件,以確保任何奇怪的活動能有一個描述性的信息。
比較全的例子實現
import autorefresh from 'jwt-autorefresh'
/** Events in your app that are triggered when your user becomes authorized or deauthorized. */
import { onAuthorize, onDeauthorize } from './events'
/** Your refresh token mechanism, returning a promise that resolves to the new access tokenFunction (library does not care about your method of persisting tokens) */
const refresh = () => {
const init = { method: 'POST'
, headers: { 'Content-Type': `application/x-www-form-urlencoded` }
, body: `refresh_token=${localStorage.refresh_token}&grant_type=refresh_token`
}
return fetch('/oauth/token', init)
.then(res => res.json())
.then(({ token_type, access_token, expires_in, refresh_token }) => {
localStorage.access_token = access_token
localStorage.refresh_token = refresh_token
return access_token
})
}
/** You supply a leadSeconds number or function that generates a number of seconds that the refresh should occur prior to the access token expiring */
const leadSeconds = () => {
/** Generate random additional seconds (up to 30 in this case) to append to the lead time to ensure multiple clients dont schedule simultaneous refresh */
const jitter = Math.floor(Math.random() * 30)
/** Schedule autorefresh to occur 60 to 90 seconds prior to token expiration */
return 60 + jitter
}
let start = autorefresh({ refresh, leadSeconds })
let cancel = () => {}
onAuthorize(access_token => {
cancel()
cancel = start(access_token)
})
onDeauthorize(() => cancel())
免責聲明:我是維護者
附: