Python實現JWT(JSON Web Token)認證
作者介紹
張龍(zero),一線運維老鳥。致力於LINUX/PYTHON/開源技術研究。
常見認證方法
首先要明白,認證和授權是不同的。認證是判定使用者的合法性,授權是判定使用者的許可權級別是否可執行後續操作。這裡所講的僅含認證。
basic認證
這是http協議中所帶帶基本認證,是一種簡單為上的認證方式。原理是在每個請求的header中新增使用者名稱和密碼的字串(格式為“username:password”,用base64編碼)。這種方式相當於將“使用者名稱:密碼”繫結為一個開放式證書,這會有幾個問題:
1.每次請求都需要使用者名稱密碼,如果此連線未使用SSL/TLS,或加密被破解,使用者名稱密碼基本就暴露了
2.無法登出使用者的登入狀態
3.證書不會過期,除非修改密碼。
cookie
將認證的結果存在客戶端的cookie中,通過檢查cookie中的身份資訊來作為認證結果。這種方式的特點是便捷,且只需要一次認證,多次可用;也可以登出登入狀態和設定過期時間;甚至也有辦法(比如設定httpOnly)來避免XSS攻擊。但它的缺點十分明顯,使用cookie那便是有狀態的服務了。
JWT
JWT協議似乎已經應用十分廣泛,JSON Web Token——一種基於token的json格式web認證方法。基本的原理是,第一次認證通過使用者名稱密碼,服務端簽發一個json格式的token。後續客戶端的請求都攜帶這個token,服務端僅需要解析這個token,來判別客戶端的身份和合法性。而JWT協議僅僅規定了這個協議的格式(RFC7519),它的序列生成方法在JWS協議中描述(https://tools.ietf.org/html/rfc7515),分為三個部分:
header頭部
header頭部主要用於宣告型別,這裡是jwt,宣告加密的演算法 通常直接使用 HMAC SHA256。一種常見的頭部是這樣的:
{ 'typ': 'JWT', 'alg': 'HS256' }
payload負荷
payload是放置實際有效使用資訊的地方。JWT定義了幾種內容,包括:標準中註冊的宣告,如簽發者,接收者,有效時間(exp),時間戳(iat,issued at)等;為官方建議但非必須公共宣告私有宣告 一個常見的payload是這樣的:
{'user_id': 123456, 'user_role': admin, 'iat': 1467255177 }
事實上,payload中的內容是自由的,按照自己開發的需要加入。
有個小問題。使用itsdangerous包的TimedJSONWebSignatureSerializer進行token序列生成的結果,exp是在頭部裡的。這裡似乎違背了jwt的協議規則。
實現JWT
如何生成token
這裡使用python模組itsdangerous,這個模組能做很多編碼工作,其中一個是實現JWS的token序列。 genTokenSeq這個函式用於生成token。其中使用的是TimedJSONWebSignatureSerializer進行序列的生成,這裡secretkey金鑰、salt鹽值從配置檔案中讀取,當然也可以直接寫死在這裡。expiresin是超時時間間隔,這個間隔以秒記,可以直接在這裡設定,選擇將其設為方法的形參。
# serializer for JWT from itsdangerous import TimedJSONWebSignatureSerializer as Serializer def genTokenSeq(self, expires): s = Serializer( secret_key=app.config['SECRET_KEY'], salt=app.config['AUTH_SALT'], expires_in=expires) timestamp = time.time() return s.dumps( {'user_id': self.user_id, 'user_role': self.role_id, 'iat': timestamp})
使用Serializer可以幫我們處理好header、signature的問題。我們只需要用s.dumps將payload的內容寫進來。這裡準備在每個token中寫入三個值:使用者id、使用者角色id和當前時間(‘iat’是JWT標準註冊宣告中的一項)。
解析token
解析需要使用到同樣的serializer,配置一樣的secret key和salt,使用loads方法來解析token。itsdangerous提供了各種異常處理類,用起來也很方便。
如果是SignatureExpired,則可以直接返回過期;
如果是BadSignature,則代表了所有其他簽名錯誤的情況,於是又分為:
- 能讀取到payload:那麼這個訊息是一個內容被篡改、訊息體加密過程正確的訊息secret key和salt很可能洩露了;
- 不能讀取到payload: 訊息體直接被篡改,secret key和salt應該仍然安全。
以上內容寫成一個函式,用於驗證使用者token。如果實現在python flask,可以考慮將此函式改為一個decorator修飾漆,將修飾器@到所有需要驗證token的方法前面,則程式碼可以更加優雅。# serializer for JWT from itsdangerous import TimedJSONWebSignatureSerializer as Serializer # exceptions for JWT from itsdangerous import SignatureExpired, BadSignature, BadData # Class xxx def tokenAuth(token): s = Serializer( secret_key=api.app.config['SECRET_KEY'], salt=api.app.config['AUTH_SALT']) try: data = s.loads(token) except SignatureExpired: msg = 'token expired' app.logger.warning(msg) return [None, None, msg] except BadSignature, e: encoded_payload = e.payload if encoded_payload is not None: try: s.load_payload(encoded_payload) except BadData: msg = 'token tampered' app.logger.warning(msg) return [None, None, msg] msg = 'badSignature of token' app.logger.warning(msg) return [None, None, msg] except: msg = 'wrong token with unknown reason' app.logger.warning(msg) return [None, None, msg] if ('user_id' not in data) or ('user_role' not in data): msg = 'illegal payload inside' app.logger.warning(msg) return [None, None, msg] msg = 'user(' + data['user_id'] + ') logged in by token.' userId = data['user_id'] roleId = data['user_role'] return [userId, roleId, msg]
原文來自微信公眾號:DevOps視角