1. 程式人生 > >Python實現JWT(JSON Web Token)認證

Python實現JWT(JSON Web Token)認證

python

作者介紹

張龍(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視角