python中jwt的使用

JWT是什麼?

  • JWT是json web token的縮寫,一種基於token的json格式web認證方法,說白了就是一個很長的字符串

JWT原理是什麼?

  • 基本的原理是,第一次認證通過用戶名密碼,服務端簽發一個json格式的token。後續客戶端的請求都攜帶這個token,服務端僅需要解析這個token,來判別客戶端的身份和合法性

JWT的作用和特點

  • 由於http協議是無狀態的,所以客戶端每次訪問都是新的請求。這樣每次請求都需要驗證身份,傳統方式是用session+cookie來記錄/傳輸用戶信息,而JWT就是更安全方便的方式。它的特點就是簡潔,緊湊和自包含,而且不佔空間,傳輸速度快,而且有利於多端分離,接口的交互等等
  • JWT的主要作用在於:可附帶用戶信息,後端直接通過JWT獲取相關信息。 使用本地保存,通過HTTP Header中的Authorization位提交驗證

JWT的組成

  • 一個通常你看到的jwt,由以下三部分組成,它們分別是:
  • 1.header:主要聲明瞭JWT的簽名算法;是一個描述JWT元數據的JSON對象
{
    "alg": "HS256",  alg屬性表示簽名使用的算法,默認爲HMAC SHA256(寫爲HS256)
    "typ": "JWT"  typ屬性表示令牌的類型,JWT令牌統一寫爲JWT
}
  • 2.payload:主要承載了各種聲明並傳遞明文數據;是存放有效信息的地方。
{
  "iss": "http://shaobaobaoer.cn",  iss: JWT的簽發者,是否使用是可選的
  "aud": "http://shaobaobaoer.cn/webtest/jwt_auth/",  aud: 接收該JWT的一方,是否使用是可選的
  "jti": "4f1g23a12aa",  jti: jwt的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊
  "iat": 1534070547,  iat(issued at): 在什麼時候簽發的(UNIX時間),是否使用是可選的
  "nbf": 1534070607,  nbf (Not Before):如果當前時間在nbf裏的時間之前,則Token不被接受;一般都會留一些餘地,比如幾分鐘;,是否使用是可選的
  "exp": 1534074147,  exp(expires): 什麼時候過期,這裏是一個Unix時間戳,是否使用是可選的
  "uid": 1,  
  "data": {
    "uname": "shaobao",
    "uEmail": "[email protected]",
    "uID": "0xA0",
    "uGroup": "guest"
  }
}
  • 3.signture:擁有該部分的JWT被稱爲JWS,也就是簽了名的JWS;沒有該部分的JWT被稱爲nonsecure JWT 也就是不安全的JWT,此時header中聲明的簽名算法爲none。三個部分用.分割。形如 xxxxx.yyyyy.zzzzz的樣式。例如:
eyJhbGciOiJIUzUxMiIsImlhdCI6MTU4NTc5MTM2MCwiZXhwIjoxNTg1NzkxNjYwfQ.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInVzZXJfcGFzc3dkIjoiMTIzNDU2IiwidXNlcl9hZGRyZXNzIjoiMTI3LjAuMC4xOjgwODAiLCJ0aW1lb3V0IjozMDAsImlhdCI6MTU4NTc5MTM2MC4xMjgxMDN9.
qMxelHUKkqPj4NI357vPgC3Zi5u8f9132mYAqlhmnAnX6PjFRtD2_bmSlOoX4XaghlLl00xMgblO7LngH6jt6A

JWT 的簽名算法有三種。

  • 1.對稱加密HMAC【哈希消息驗證碼】 HS256/HS384/HS512
    這種加密方式沒有公鑰,私鑰之分, 也就是隻有一個密鑰, 這種加密方式適用於: 服務器將生成的jwt發送給接收方, 接收方將其返回給服務器, 服務器解析 jwt, 完成身份驗證.
    首先,需要指定一個密碼(secret)。該密碼僅僅爲保存在服務器中,並且不能向用戶公開。然後,使用標頭中指定的簽名算法生成簽名
  • 2.非對稱加密RSASSA【RSA簽名算法】RS256/RS384/RS512
  • 3.ECDSA【橢圓曲線數據簽名算法】 ES256/ES384/ES512

不同的簽名適用場景

  • 對稱的算法HMAC適用於單點登錄,一對一的場景中。速度很快。
  • 但是面對一對多的情況,比如一個APP中的不同服務模塊,需要JWT登錄的時候,主服務端【APP】擁有一個私鑰來完成簽名即可,而用戶帶着JWT在訪問不同服務模塊【副服務端】的時候,副服務端只要用公鑰來驗證簽名就可以了。從一定程度上也減少了主服務端的壓力。
  • 當然,還有一種情況就是不同成員進行開發的時候,大家可以用統一的私鑰來完成簽名,然後用各自的公鑰去完成對JWT的認證,也是一種非常好的開發手段。
  • 構建一個沒有多個小型“微服務應用程序”的應用程序,並且開發人員只有一組的,選擇HMAC來簽名即可。其他情況下,儘量選擇RSA

JWT的幾點說明

  • 1、JWT默認不加密,但可以加密。生成原始令牌後,可以使用改令牌再次對其進行加密。
  • 2、當JWT未加密時,一些私密數據無法通過JWT傳輸。
  • 3、JWT不僅可用於認證,還可用於信息交換。善用JWT有助於減少服務器請求數據庫的次數。
  • 4、JWT的最大缺點是服務器不保存會話狀態,所以在使用期間不可能取消令牌或更改令牌的權限。也就是說,一旦JWT簽發,在有效期內將會一直有效。
  • 5、JWT本身包含認證信息,因此一旦信息泄露,任何人都可以獲得令牌的所有權限。爲了減少盜用,JWT的有效期不宜設置太長。對於某些重要操作,用戶在使用時應該每次都進行進行身份驗證。
  • 6、爲了減少盜用和竊取,JWT不建議使用HTTP協議來傳輸代碼,而是使用加密的HTTPS協議進行傳輸。

實例代碼

  • jwt.py
import time
import configparser
from scripts.response import Resp
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, SignatureExpired, BadSignature


class JWTKeyGen:
    secret_key = '4180da82-0c83-4d66-ab14-e2793573ecaa'  # 密鑰,默認不可變,更改需要與服務器溝通
    salt = '16fcf475-5180-4916-83c1-5ff79616eaa9'  # 加密鹽,隨機字符串,默認不可變,更改需要與服務器溝通
    expires = 300  # token過期時長 單位:S
    jti = '4f1g23a12aa'  # jwt的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊
    space = 600  # 間隔時間 單位:S  注意用於生成nbf:nbf = iat(簽發時間) - space

    config_path = "../config/jwt.conf"  # 配置文件路徑
    
    '''
    獲取配置文件中指定key的value值
    : param key: 配置參數名稱
    : return: 返回key對應的value字符串
    '''
    def get_config_info(self, key):
        conf = configparser.ConfigParser()
        conf.read(JWTKeyGen.config_path)
        try:
            value = conf.get("JWT", key)
            if value == '':
                value = self.compare_key(key)
        except Exception:
            value = self.compare_key(key)
        return value

    '''
    比對key的名稱,不同的key賦不同的value
    : param key: 配置參數名稱
    : return: 返回key對應的value字符串
    '''
    def compare_key(self, key):
        if key == 'secret_key':
            value = self.secret_key
        elif key == 'salt':
            value = self.salt
        elif key == 'expires':
            value = self.expires
        elif key == 'jti':
            value = self.jti
        else:
            value = self.space
        return value
    
    '''
    生成token
    :param username: 用戶名
    :param password: 用戶密碼
    :param address: 服務器地址
    :return: 返回token字符串
    '''
    def gen_token_seq(self, username, address):
        expires = self.get_config_info("expires")
        # 序列化
        s = Serializer(
            salt=self.get_config_info("salt"),
            secret_key=self.get_config_info("secret_key"),
            expires_in=int(self.get_config_info("expires"))
        )
        timestamp = time.time()  # 獲取當前時間戳,單位 S
        # 有效數據信息
        json_str = {
            'iat': timestamp,  # iat(issuedat): 在什麼時候簽發的(UNIX時間),是否使用是可選的
            "iss": "www.xxx.com",  # iss: JWT的簽發者,是否使用是可選的
            "aud": "www.xxx.com",  # aud: 接收該JWT的一方,是否使用是可選的
            "jti": self.get_config_info("JTI"),  # jti: jwt的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊,是否使用是可選的
            # nbf(Not Before):如果當前時間在nbf時間之前,則Token不被接受;一般都會留一些餘地,比如幾分鐘;是否使用是可選的
            "nbf": int(timestamp) - int(self.get_config_info("space")),
            "exp": int(expires),   # exp(expires): 什麼時候過期,這裏是一個Unix時間戳,是否使用是可選的
            "uid": 1,   # 用戶id,此處寫成死值,如有需要可查庫獲取,是否使用是可選的
            "data": {
                'username': username,
                # 'password': password,
                'address': address,
            }
        }
        return s.dumps(json_str)
    
    '''
    token解析
    :param token: jwt字符串
    :return: 返回解密後的有效數據
    '''
    def token_auth(self, token):
    
        if token is None:
            return Resp().get_error('參數token爲空')
        s = Serializer(salt=self.get_config_info("salt"), secret_key=self.get_config_info("secret_key"))  # 序列化
        try:
            data = s.loads(token)  # token解析
        except SignatureExpired:  # token過期
            return Resp().get_error('該token已過期')
        except BadSignature:  # 簽名出錯
            return Resp().get_error('簽名出錯')
        except:  # 其他異常
            return Resp().get_error('未知原因出錯')
        # 判斷簽名是否被篡改
        if ('username' not in data['data']) or ('address' not in data['data']):
            return Resp().get_error('載荷數據不合法, 非法操作')
        # 獲取有效信息數據並返回
        info = {
            "username": data['data']['username'],
            # "password": data['data']['password'],
            "address": data['data']['address']
        }
    
        return Resp().get_ok('OK', data=info)

    '''
    token校驗
    :param token: token字符串
    :return True / False
    '''
    def verify_token(self, token):
        login_info = self.token_auth(token)
        if login_info.get('code') == 200:
            username = login_info.get('data').get('username')
            # password = login_info.get('data').get('password')
            address = login_info.get('data').get('address')
            return True if self.gen_token_seq(username, address) == token else False
        else:
            return login_info

if __name__ == "__main__":

    name = "admin"
    # password = "123456"
    address = "127.0.0.1:8080"

    token = JWTKeyGen().gen_token_seq(name, address)
    print("生成的token----:" + str(token))

    login_info = JWTKeyGen().token_auth(token)
    if login_info.get('code') == 200:

        print("用戶名:" + login_info.get('data').get('username'))
        # print("密碼:" + login_info.get('data').get('password'))
        print("url:" + login_info.get('data').get('address'))
        print("消息:" + login_info.get('msg'))
    else:
        print("消息:" + login_info.get('msg'))
  • jwt.conf
[JWT]

; 密鑰,默認不可變,更改需要與服務器溝通
secret_key = 4180da82-0c83-4d66-ab14-e2793573ecaa

; 加密鹽,隨機字符串,默認不可變,更改需要與服務器溝通
salt = 16fcf475-5180-4916-83c1-5ff79616eaa9

; token過期時長 單位:S
expires = 300

; jwt的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊
jti = 4f1g23a12aa

; 間隔時間 單位:S  注意用於生成nbf:nbf = iat(簽發時間) - space
space = 600
  • response.py

class Resp:
    '''
    出錯返回
    :param msg: 錯誤消息說明
    :param data: 返回的數據
    :return {
            "code": 500,
            "data": data,
            "msg": msg
        }
    '''
    def get_error(self, msg, data=None):
        res = {
            "code": 500,
            "data": data,
            "msg": msg
        }
        return res

    '''
    正常返回
    :param msg: 返回消息說明
    :param data: 返回的數據
    :return {
            "code": 200,
            "data": data,
            "msg": msg
        }
    '''
    def get_ok(self, msg, data=None):
        res = {
            "code": 200,
            "data": data,
            "msg": msg
        }
        return res
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章