JWT 避坑指南:nbf 驗籤失效問題的解決
現象
剛簽發的 JWT,在下一個請求使用時候會失效,請求會報 422 錯誤。
{ "msg": "The token is not yet valid (nbf)" }
如果隔幾秒再請求(例如使用 Chrome 開發者工具中的 Replay XHR),就會成功。

nbf 欄位的原理
檢視上面的報錯資訊,會發現有一個 nbf,nbf 是 JWT 協議中的一個欄位,是 Not Before 的縮寫,表示 JWT Token 在這個時間之前是無效的,一般來講會設定成簽發的時間。這裡產生了一個猜想,多伺服器環境時候,伺服器之間時間如果不一致,一臺伺服器簽發的 token 如果立刻被髮往另一臺伺服器驗證,就很容易產生 nbf 欄位驗證不通過的問題。其實 JWT 協議已經考慮到了這類問題,所以協議中在 nbf 這一節專門提到了可以使用一個 small leeway 來解決這個問題。
4.1.5. "nbf" (Not Before) Claim
The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. The processing of the "nbf" claim requires that the current date/time MUST be after or equal to the not-before date/time listed in the "nbf" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL.
既然文件考慮到了這個問題,我們再來看一下程式碼是怎麼實現的,我們使用的是 flask-jwt-extended 這個庫來實現 JWT 的簽發和驗籤,flask-jwt-extended 依賴的是 PyJWT 這個庫,所以在 PyJWT 原始碼中查詢一下這個錯誤。
def _validate_nbf(self, payload, now, leeway): try: nbf = int(payload['nbf']) except ValueError: raise DecodeError('Not Before claim (nbf) must be an integer.') if nbf > (now + leeway): raise ImmatureSignatureError('The token is not yet valid (nbf)')
可以看出,這個報錯的確是在驗證 nbf 欄位時候出現的,如果 nbf 的時間晚於當前的時間加上一個 leeway,就會丟擲錯誤,而從 flask_jwt_extended 原始碼中可以看到,這個 leeway 欄位是使用者設定的,而我們設定為了 0,也就是說不存在餘量時間,這就要求伺服器之間的時間同步,才能不出現 nbf 欄位驗證不通過的問題。
驗證問題
後端應用跑在多個節點中,使用 ansible 來同時獲取多臺機器的時間。
ansible machine_group -m command -a 'date'
需要注意的是,ansible 預設的併發數是 5,機器多的情況下需要修改 ansible.cfg 中的 forks,這樣能保證獲取時間的操作儘可能在同一時間發起。
[defaults] host_key_checking = False forks = 10

可以看到,不同的機器上的時間並沒有同步,並且差異比較大,甚至達到了 2 分鐘,這樣無疑會造成 nbf 欄位驗籤不通過。
解決問題:配置 Linux 自動時間同步
因為多個伺服器節點之間時間差太大,所以首先解決伺服器之間時間不同步的問題,以 Ubuntu 為例,步驟如下:
安裝 Chrony。
sudo apt install chrony
安裝後 chrony 就會和預設 ntp 伺服器進行同步,各個雲環境都有自己的 ntp 伺服器,在 /etc/chrony/chrony.conf
中可以配置首選 ntp 伺服器,例如 aws 環境,需要在所有伺服器前增加如下伺服器。實測 aws 環境中並不能使用其他的 ntp 伺服器(包括國家授時中心、阿里雲 ntp 伺服器)。
server 169.254.169.123 prefer iburst
重啟 chrony 服務。
sudo systemctl restart chrony
檢視是否生效。
sudo chronyc tracking
如果狀態中有如下語句表示正常
Leap status : Normal
將所有節點同步過時間後,再次測試,發現問題消失。
上面過程是所有伺服器節點都與時間伺服器的時間進行同步,如果在網路隔離的環境中,可以選擇一臺節點作為授時伺服器,其他節點與這臺伺服器進行時間同步。
更進一步:增加 leeway
雖然同步時間過後問題已經消失,但是伺服器之間仍然可能會產生微小的時間差,可以通過增加 leeway 來覆蓋這種偶發的場景,但是 leeway 也不能無限加長,時間太長會造成安全性下降。