Python - JWT (JSON Web Token)
Posted on Dec 1, 2018 in Python 模組/套件推薦 by Amo Chen ‐ 3 min read
JWT(JSON Web Token) 是 RFC 7519 定義的一套標準,用以確保應用(application)之間傳遞訊息的安全性與完整性(integrity)。 JWT 常常與傳統的 Cookie/Session 技術一起被比較,然而這些技術是為了解決不同問題所發明的,也有各自的優缺點與特別合適的應用場景,沒有誰優誰劣的絕對定論。
目前實務上也越來越多應用會利用 JWT 傳遞資料,譬如 APP 在使用者登入時透過 JWT 取得常用的「非機敏性資料」(例如,暱稱、語系設定等等),並且儲存在裝置內,以減少詢問伺服器的次數,達到節省伺服器資源與增加下一次 APP 啟動速度的效果,運用得當的話也是一個加分的技術。
至於為什麼特別強調「非機敏性資料」,本文稍後將作解釋,先一起透過 Python 學習 JWT 相關的概念與使用吧!
本文環境
- Python 3.6.5
- PyJWT 1.6.4
安裝 PyJWT
$ pip install PyJWT
JWT 技術原理
JWT 是個很長的字串,例如:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIn0.bqxXg9VwcbXKoiWtp-osd0WKPX307RjcN7EuXbdq-CE
這個字串會用 .
進行分隔,分成 3 個部分:
- Header
- Payload
- Signature (簽章)
如果用上面的範例進行對應就是:
Header: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Payload: eyJoZWxsbyI6IndvcmxkIn0
Signature: bqxXg9VwcbXKoiWtp-osd0WKPX307RjcN7EuXbdq-CE
其中 Header 與 Payload 是經過 Base64URL 編碼過的結果,所以能夠用 Base64URL 進行解碼(decode):
Header: {"typ":"JWT","alg":"HS256"}
Payload: {"hello":"world"}
而 Signature 是 Header 與 Payload 2 個部分加在一起所做的簽章,簽章的函數為(預設使用 HMAC SHA256,了解原理之後,也可以將 HMAC SHA256 換成其他簽章的演算法):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
密鑰(secret key)
)
p.s. 密鑰只有產生 JWT 的伺服器端才擁有,因此也只有產生該 JWT 的伺服器才能驗證其正確性(千萬別在前端產生 JWT)
也因為有了簽章,只要伺服器端對簽章進行驗證,就可以知道 JWT 是否被竄改,以確保其可靠性。
JWT 就是透過如此方式所組成的字串,也由於 Header & Payload 是可以解開的,所以 不可以將機敏性的資料(例如密碼)放在其中 ,否則很容易就會被解開導致資料外洩。
以上就是 JWT 的技術原理,接著實際用 PyJWT 產生 JWT 看看吧!
用 PyJWT 產生 JSON Web Token
產生 JWT 的方法很簡單:
>>> import jwt
>>> jwt.encode({'hello': 'world'}, 'secret', algorithm='HS256')
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIn0.bqxXg9VwcbXKoiWtp-osd0WKPX307RjcN7EuXbdq-CE'
其中 secret
就是密鑰(不可外洩,否則人人都能夠做出一樣的 JWT),而 HS256
則是簽章的算法,也就是預設的 HMAC SHA526 。目前 PyJWT 支援 RS256
與 HS256
2 種簽章算法。
JWT 使用方式
一般來說前端拿到 JWT 可以放在 Cookie, LocalStorage 或者 HTTP Header 中,每次與後端伺服器 API 溝通時,就得提供此 Token 給後端伺服器驗證。
JWT 的問題
產生 JWT 之後,還是有後續的問題得考慮,既然 JWT 是當作 Token 來使用,那麼 Token 由誰發出、何時生效、何時過期等,要如何驗證?
JWT 目前提供 7 種關鍵字,可以放在 Payload 內,讓後端伺服器可以根據這些關鍵字判斷 Token 有效與否:
iss
(Issuer) Token 的發行者sub
(Subject) 也就是使用該 Token 的使用者aud
(Audience) Token 的接收者,也就是後端伺服器exp
(Expiration Time) Token 的過期時間nbf
(Not Before) Token 的生效時間iat
(Issued At) Token 的發行時間jti
(JWT ID) Token 的 ID
所以 1 個嚴謹的 JWT Token 可能會這樣產生:
>>> import jwt
>>> from datetime import datetime
>>> payload = {
... 'iss': 'example.com',
... 'sub': 'the_user_id',
... 'aud': 'www.example.com',
... 'exp': datetime.utcnow(), # must use UTC time
... 'nbf': datetime.utcnow(),
... 'iat': datetime.utcnow(),
... 'jti': 'unique_jwt_id',
... 'hello': 'world',
... }
>>> jwt.encode(payload, 'secret', algorithm='HS256')
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsInN1YiI6InRoZV91c2VyX2lkIiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwiZXhwIjoxNTQzNjUwMjg3LCJuYmYiOjE1NDM2NTAyODcsImlhdCI6MTU0MzY1MDI4NywianRpIjoidW5pcXVlX2p3dF9pZCJ9.FXnVl2ZOQTrPV-LRstVEm_Bi2em7b85fobt6BjTe5PA'
如果前端將上述 JWT 傳送至後端時,後端可以使用 jwt.decode
函示試著解開該 JWT:
>>> jwt.decode(b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsInN1YiI6InRoZV91c2VyX2lkIiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwiZXhwIjoxNTQzNjUwMjg3LCJuYmYiOjE1NDM2NTAyODcsImlhdCI6MTU0MzY1MDI4NywianRpIjoidW5pcXVlX2p3dF9pZCJ9.FXnVl2ZOQTrPV-LRstVEm_Bi2em7b85fobt6BjTe5PA', 'secret', audience='www.example.com', issuer='example.com')
{'iss': 'example.com', 'sub': 'the_user_id', 'aud': 'www.example.com', 'exp': 1543659351, 'nbf': 1543649351, 'iat': 1543649351, 'jti': 'unique_jwt_id', 'hello': 'world'}
正確的話會得到 Payload 回傳。
如果是假造的 Token 的話則會拋出 Exception:
jwt.exceptions.InvalidSignatureError: Signature verification failed
如果是過期的 Token 則會拋出 Exception:
ExpiredSignatureError: Signature has expired
如果 Token 的發行者 (issuer) 有問題的話,則會拋出 Exception:
InvalidIssuerError: Invalid issuer
這邊還要注意一件事,目前 PyJWT 只支援以下 5 個關鍵字的驗證:
- “exp” (Expiration Time) Claim
- “nbf” (Not Before Time) Claim
- “iss” (Issuer) Claim
- “aud” (Audience) Claim
- “iat” (Issued At) Claim
所以 sub
與 jti
的驗證,還是得在後端額外利用程式驗證,例如驗證 sub
的資料是否正確、 jti
所對應的 Token 是否被停用等等。
如此一來, JWT 的安全性就相對有保證一些。
以上就是 JWT 相關的概念與使用囉, Happy Coding!
References
https://jwt.io/introduction/
https://tools.ietf.org/html/rfc7519
https://pyjwt.readthedocs.io/en/latest/index.html