天天看點

使用python實作背景系統的JWT認證

今天的文章介紹一種适用于restful+json的API認證方法,這個方法是基于jwt,并且加入了一些從oauth2.0借鑒的改良。

首先要明白,認證和鑒權是不同的。認證是判定使用者的合法性,鑒權是判定使用者的權限級别是否可執行後續操作。這裡所講的僅含認證。認證有幾種方法:

這是http協定中所帶帶基本認證,是一種簡單為上的認證方式。原理是在每個請求的header中添加使用者名和密碼的字元串(格式為“username:password”,用base64編碼)。

這種方式相當于将“使用者名:密碼”綁定為一個開放式證書,這會有幾個問題:①每次請求都需要使用者名密碼,如果此連接配接未使用SSL/TLS,或加密被破解,使用者名密碼基本就暴露了;②無法登出使用者的登入狀态;③證書不會過期,除非修改密碼。

總體來說,這種方法的特點就是,簡單但不安全。

将認證的結果存在用戶端的cookie中,通過檢查cookie中的身份資訊來作為認證結果。這種方式的特點是便捷,且隻需要一次認證,多次可用;也可以登出登入狀态和設定過期時間;甚至也有辦法(比如設定httpOnly)來避免XSS攻擊。但它的缺點十分明顯,使用cookie那便是有狀态的服務了。

①聲明類型,這裡是jwt

②聲明加密的算法 通常直接使用 HMAC SHA256,一種常見的頭部是這樣的:

再将其進行base64編碼。

payload是放置實際有效使用資訊的地方。JWT定義了幾種内容,包括:

①标準中注冊的聲明,如簽發者,接收者,有效時間(exp),時間戳(iat,issued at)等;為官方建議但非必須;

②公共聲明;

③私有聲明;

一個常見的payload是這樣的:

事實上,payload中的内容是自由的,按照自己開發的需要加入。 Ps.有個小問題。使用itsdangerous包的TimedJSONWebSignatureSerializer進行token序列生成的結果,exp是在頭部裡的。這裡似乎違背了jwt的協定規則。

存儲了序列化的secreate key和salt key。這個部分需要base64加密後的header和base64加密後的payload使用.連接配接組成的字元串,然後通過header中聲明的加密 方式進行加鹽secret組合加密,然後就構成了jwt的第三部分。

目标場景是一個前後端分離的後端系統,用于運維工作,雖在内網使用,也有一定的保密性要求。

①API為restful+json的無狀态接口,要求認證也是相同模式

②可橫向擴充

③較低資料庫壓力

④證書可登出

⑤證書可自動延期

⑥選擇JWT。

這裡使用python子產品itsdangerous,這個子產品能做很多編碼工作,其中一個是實作JWS的token序列。genTokenSeq 這個函數用于生成token。其中使用的是TimedJSONWebSignatureSerializer進行序列的生成,這裡secret_key密 鑰、salt鹽值從配置檔案中讀取,當然也可以直接寫死在這裡。expires_in是逾時時間間隔,這個間隔以秒記,可以直接在這裡設定,我選擇将其設 為方法的形參(因為這個函數也用在了解決下提到的問題2)。

使用這個Serializer可以幫我們處理好header、signature的問題。我們隻需要用s.dumps将payload的内容寫進來。這裡我準備在每個token中寫入三個值:使用者id、使用者角色id和目前時間(‘iat’是JWT标準注冊聲明中的一項)。

假設我所寫入的資訊是

采用以上的方法所生成的token為

它是由“header.payload.signature”構成的。

解析需要使用到同樣的serializer,配置一樣的secret key和salt,使用loads方法來解析token。itsdangerous提供了各種異常處理類,用起來也很友善,如果是SignatureExpired,則可以直接傳回過期;如果是BadSignature,則代表了所有其他簽名錯誤的情況,于是又分為:

①能讀取到payload:那麼這個消息是一個内容被篡改、消息體加密過程正确的消息,secret key和salt很可能洩露了;

②不能讀取到payload: 消息體直接被篡改,secret key和salt應該仍然安全。

以上内容寫成一個函數,用于驗證使用者token。如果實作在python flask,可以考慮将此函數改為一個decorator修飾漆,将修飾器@到所有需要驗證token的方法前面,則代碼可以更加優雅。

檢查和判定的機制如下:

1、使用加密的類,再用來解密(用上之前的密鑰和鹽值),得到結果存入data;

2、如果捕獲到SignatureExpired異常,則代表根據token中的expired設定,token已經逾時失效,傳回‘token expired’;

3、如果是其他BadSignature異常,又要分為:

4、如果payload還完整,則解析payload,如果捕獲BadData異常,則代表token已經被篡改,傳回‘token tampered’;

5、如果payload不完整,直接傳回‘badSignature of token’;

6、如果以上異常都不對,那隻能傳回未知異常‘wrong token with unknown reason’;

7、最後,如果data能正常解析,則将payload中的資料取出來,驗證payload中是否有合法資訊(這裡是user_id和 user_role鍵值的json資料),如果資料不合法,則傳回‘illegal payload inside’。一旦出現這種情況,則代表密鑰和鹽值洩露的可能性很大。

上述的方法可以做到基本的JWT認證,但在實際開發過程中還有其他問題:

token在生成之後,是靠expire使其過期失效的。簽發之後的token,是無法收回修改的,是以涉及token的有效期的更改是個難題,它展現在以下兩個問題:

使用python實作背景系統的JWT認證

問題1.使用者登出

使用python實作背景系統的JWT認證

問題2.token自動延期

如何解決更改token有效期的問題,網上看到很多讨論,主要集中在以下内容:

使用python實作背景系統的JWT認證

JWT是一次性認證完畢加載資訊到token裡的,token的資訊内含過期資訊。過期時間過長則被重播攻擊的風險太大,而過期時間太短則請求端體驗太差(動不動就要重新登入)

使用python實作背景系統的JWT認證

把token存進庫裡,很自然能想到的是把每個token存庫,設定一個valid字段,一旦登出了就valid=0;設定有效期字段,想要延期 就增加有效期時間。openstack keystone就是這麼做的。這個做法雖友善,但對資料庫的壓力較大,甚至在通路量較大,簽發token較多的情況下,是對資料庫的一個挑戰。況且這也 有悖于JWT的初衷。

使用python實作背景系統的JWT認證

為了使使用者不需要經常重新登入,用戶端将使用者名密碼儲存起來(cookie),然後使用使用者名密碼驗證,但那還得考慮防禦CSRF攻擊的問題。

使用python實作背景系統的JWT認證

一個為access token,用于使用者後續的各個請求中攜帶的認證資訊

使用python實作背景系統的JWT認證

另一個是refresh token,為access token過期後,用于申請一個新的access token。

由此可以給兩類不同token設定不同的有效期,例如給access token僅1小時的有效時間,而refresh token則可以是一個月。api的登出通過access token的過期來實作(前端則可直接抛棄此token實作登出),在refresh token的存續期内,通路api時可執refresh token申請新的access token(前端可存此refresh token,access token過其實進行更新,達到自動延期的效果)。refresh token不可再延期,過期需重新使用使用者名密碼登入。

這種方式的理念在于,将證書分為三種級别:

使用python實作背景系統的JWT認證

access token 短期證書,用于最終鑒權

使用python實作背景系統的JWT認證

refresh token 較長期的證書,用于産生短期證書,不可直接用于服務請求

使用python實作背景系統的JWT認證

使用者名密碼 幾乎永久的證書,用于産生長期證書和短期證書,不可直接用于服務請求

通過這種方式,使證書功效和證書時效結合考慮。

ps.前面提到建立token的時候将expire_in(jwt的推薦字段,逾時時間間隔)作為函數的形參,是為了将此函數用于生成access token和refresh token,而兩者的expire_in時間是不同的。

我們做了一個JWT的認證子產品:

(access token在以下代碼中為'token',refresh token在代碼中為'rftoken')

使用python實作背景系統的JWT認證

首次認證

client --使用者名密碼---> server

client <--token、rftoken-- server

使用python實作背景系統的JWT認證

access token存續期内的請求

client --請求(攜帶token)--> server

client <---結果----server

使用python實作背景系統的JWT認證

access token逾時

client ---請求(攜帶token)--> server

client <----msg:token expired-- server

使用python實作背景系統的JWT認證

重新申請access token

client -請求新token(攜帶rftoken)-> server

client <-----新token----- server

使用python實作背景系統的JWT認證

rftoken token逾時

client <----msg:rftoken expired--- server

如果設計一個針對此認證的前端,需要:

使用python實作背景系統的JWT認證

存儲access token、refresh token

使用python實作背景系統的JWT認證

通路時攜帶access token,自動檢查access token逾時,逾時則使用refresh token更新access token;狀态延期使用者無感覺

使用python實作背景系統的JWT認證

使用者登出直接抛棄access token與refresh token

原文釋出時間為:2017-04-10

本文作者:茶客furu聲