JWT 的 Token 過期時間為什么沒有生效
在我第一次在 DRF(Django REST Framework)中使用 JWT 時,感覺 JWT 非常神奇,它即沒有使用 session、cookie,也不使用數(shù)據(jù)庫,僅靠一段加密的字符串,就解決了用戶身份驗證的煩惱。
直到我遇到了一個當時百思不得解的問題,才揭開了它的神秘面紗。
當時遇到的問題就是,無論怎么設(shè)置 JWT TOKEN 的過期時間,都沒有生效,即使設(shè)置為 1 秒后過期,過了 1 分鐘,TOKEN 還是可以正常使用,重啟 Django 服務(wù)也不行。
沒有別的辦法,我就硬著頭皮去追著源碼,看看 JWT 是怎么判斷 TOKEN 是否過期的。
具體的方法就是,深度優(yōu)先追溯 JWT 代碼的源頭。在 DRF 中,配置了 DEFAULT_AUTHENTICATION_CLASSES 就是 JWT:
直接定位至這個類,發(fā)現(xiàn)它繼承了 BaseJSONWebTOKENAuthentication
然后看 BaseJSONWebTOKENAuthentication,發(fā)現(xiàn)有一段判斷過期的邏輯:
繼續(xù)展開 jwt_decode_handler 這個函數(shù),發(fā)現(xiàn)它調(diào)用了 jwt.decode 函數(shù)
展開 jwt.decode 函數(shù),發(fā)現(xiàn)它調(diào)用了函數(shù) _validate_claims
函數(shù) _validate_claims 又調(diào)用了 _validate_exp,
然后展開 _validate_exp,找到了這段:
發(fā)現(xiàn)過期時間 exp 來自 payload,payload 又來自 TOKEN 本身:
至此謎底揭開,原來,TOKEN 的過期時間其實被編碼在了 TOKEN 本身,服務(wù)器收到 TOKEN 時先進行解碼,解碼出過期時間,然后和當前時間進行對比,如果當前時間比較小,說明沒有過期,TOKEN 就是有效的,否則返回客戶端 "Signature has expired."
我 Debug 出了這個 TOKEN 的過期時間 exp,發(fā)現(xiàn)這個 exp 是修改 JWT_EXPIRATION_DELTA 之前的那個過期時間,原來修改 JWT_EXPIRATION_DELTA 之后需要重新生成 TOKEN,這樣的過期時間才會按照新的來。
至此,JWT 的原理已經(jīng)非常清晰了:
用戶第一次登錄時,服務(wù)器(JWT)會獲得用戶名、用戶 id,在加上設(shè)置的過期時間構(gòu)建 payload:
- payload = {
- 'user_id': user.pk,
- 'username': username,
- 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA
- }
然后將 payload 用設(shè)置好的算法使用私鑰加密成 token
- def jwt_encode_handler(payload):
- key = api_settings.JWT_PRIVATE_KEY or jwt_get_secret_key(payload)
- return jwt.encode(
- payload,
- key,
- api_settings.JWT_ALGORITHM
- ).decode('utf-8')
token 返回至客戶端后,客戶端緩存該 token,然后每一次請求時都帶上該 token。
服務(wù)器在收到請求時先驗證該 token,驗證的過程就是對 token 進行逆向解碼:
- def jwt_decode_handler(token):
- options = {
- 'verify_exp': api_settings.JWT_VERIFY_EXPIRATION,
- }
- # get user from token, BEFORE verification, to get user secret key
- unverified_payload = jwt.decode(token, None, False)
- secret_key = jwt_get_secret_key(unverified_payload)
- return jwt.decode(
- token,
- api_settings.JWT_PUBLIC_KEY or secret_key,
- api_settings.JWT_VERIFY,
- options=options,
- leeway=api_settings.JWT_LEEWAY,
- audience=api_settings.JWT_AUDIENCE,
- issuer=api_settings.JWT_ISSUER,
- algorithms=[api_settings.JWT_ALGORITHM]
- )
解密使用同樣的算法,使用公鑰或私鑰進行解密,解密成功且不過期,則認為用戶有權(quán)限訪問,正常返回。
最后
這個問題至少花了我半個小時的時間,如果你遇到這種情況,能瞬間明白其中緣由,那本文的目的就達到了。
源碼之下無秘密,遇到問題,去看源碼可能不是解決問題最快的方法,卻是提升自己最快的方法。很多開源軟件設(shè)計模式的應(yīng)用都非常值得我們學習,比如 DRF 的模塊設(shè)計,通過 mixins 組合來實現(xiàn)靈活可擴展的 APIView,通過子類傳入相關(guān)的 class 來實現(xiàn)用戶自定義的功能。如何寫出靈活可擴展、高內(nèi)聚低耦合、符合開閉原則的程序,閱讀開源代碼,是一個非常高效的學習方式。
當然了,這需要先對設(shè)計模式有一個系統(tǒng)的學習,讓自己有一雙慧眼,不然就是守著金山不自知。
本文轉(zhuǎn)載自微信公眾號「Python七號」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系Python七號公眾號。