在读本文之前需要了解的知识点:
PKI体系:Public Key Infrastructure
公钥:用来加密和验签(验签的过程其实就是用公钥解密出签名内容,与原签名内容对比是否一致)
私钥:用来解密和签名(签名的过程其实就是用私钥加密签名内容,然后将签名与签名内容一同发给对方)
关于加密与签名的区别,请参考我的另外一篇博文:接口安全:谈谈加密与签名的区别_签名和加密的区别_boweiqiang的博客-CSDN博客
为了 理解jwt的验签过程,自己写的测试验证代码如下:
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File : jwt_test.py
@Contact : boweiqiang@163.com
@MTime : 2020-04-26 11:32
@Author: boweiqiang
@Version: 1.0
@Desciption:
jwt的签名算法JWT 的签名算法有三种:
1.对称加密HMAC【哈希消息验证码】 HS256/HS384/HS512
2.非对称加密RSASSA【RSA签名算法】RS256/RS384/RS512
3.ECDSA【椭圆曲线数据签名算法】 ES256/ES384/ES512,RSA签名
这里实验的是RSA签名和验签的过程
注意:本文中用的jwt是:https://pypi.org/project/jwt/,安装方法为:pip3 install jwt
注意与python-jwt区别:https://pypi.org/project/python-jwt/,安装方法为:pip install python_jwt
'''
import time
import base64
from Crypto.PublicKey import RSA
from jwt import JWT, jwk_from_pem
print('-----------生成RSA密钥对--------------')
rsa_key = RSA.generate(1024)
rsa_private_key = rsa_key.export_key()
rsa_public_key = rsa_key.public_key().export_key()
print(rsa_private_key.decode('utf-8'))
print(rsa_public_key.decode('utf-8'))
print('-----------生成jwt--------------')
exp_time = int(time.time() + 60)
payload_origin = {'userId': 123456, 'name': 'mogui', 'exp': exp_time}
jwt_pri_key = jwk_from_pem(rsa_private_key)
jwt = JWT()
jwt_token = jwt.encode(payload_origin, jwt_pri_key, alg='RS256', optional_headers={'ent': 'esign'})
print('token:', jwt_token)
# # 事先准备好经过rsa私钥加签过的jwt token
# jwt_token = 'eyJhbGciOiJSUzI1NiJ9.eyJvcGVyYXRpb25Vc2VySWQiOiJlZTgzMjYyYThkNGIxMWU3YTdiMzZjOTJiZjMxNjA3YiIsInBob25lIjoiMTU3NTcxNjA1MzEiLCJsb2dpbk5hbWUiOiIxNTc1NzE2MDUzMSIsIm5hbWUiOiLmtYvor5XmlLkxIiwiZXhwIjoxNTg3NzA5OTY3LCJyb2xlTmFtZXMiOiJjY-i_kOiQpeS6uuWRmCzku5PnrqHlkZgs5ZWG5ZOB566h55CG5ZGYLOacuuaehOi0n-i0o-S6uizmtYvor5XlkZgs57O757uf566h55CG5ZGYLOiuouWNleeuoeeQhizotYTmlpnnrqHnkIblkZgifQ.Ugzrowz1TD38IK5yuHAxoCURGxByBm0Ep9JtvitijhFw3MGqooaRDxKIOUk6aAFVuV6zfpY7y23oCk3-KsBvVR1oVLIAWSxiRJlVs-sne2vTEplH2xuzQv2N1HuAGvrbYlhMu2rIqitP7D5xQjVruBTmalO9TZUgsc8ONIMBZH4'
# print('token:', jwt_token)
# # 将事先准备好的公钥标准化
# rsa_public_key_str = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC84nyOnK4BGDuk/EsKNxgTjoeLqSmHWR++H0VgLdfk+as8tuBLD0HSv42wYWfBi+ICqwo+ylHFD9TM6Xx2J2ojxO6uHirCgmsdD9WzCAqannNU3YuNjFYBJI+b7N23IaAfB4ZRQKr4h0ZO2f4ruW8JUUSbMZunhtBTErurRfqK7QIDAQAB'
# if isinstance(rsa_public_key_str, str):
# rsa_public_key = rsa_public_key_str.encode()
# if not rsa_public_key.startswith(b'-'):
# rsa_public_key = b'-----BEGIN PUBLIC KEY-----\n' + rsa_public_key + b'\n-----END PUBLIC KEY-----'
print('-----------base64解密jwt中的header和payload--------------')
# 知识点:jwt token分为三个部分,用.分隔:header, payload, signature
jwt_token_parts = jwt_token.split('.')
header = jwt_token_parts[0]
payload = jwt_token_parts[1]
signature = jwt_token_parts[2]
print('header:', header)
print('payload:', payload)
print('sign:', signature)
'''
知识点:jwt三个部分其实都是base64加密的字符串,并且是urlsafe的base64加密字符串
知识点:什么是urlsafe的base64?标准的Base64并不适合直接放在URL里传输,因为URL编码器会把标准Base64中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为ANSI SQL中已将“%”号用作通配符。
为解决此问题,可采用一种用于URL的改进Base64编码,它去除了在末尾填充'='号,并将标准Base64中的“+”和“/”分别改成了“-”和“_”,这样就免去了在URL编解码和数据库存储时所要作的转换,避免了编码信息长度在此过程中的增加,并统一了数据库、表单等处对象标识符的格式。
如果要解密urlsafe的base64加密字符串,可以用python系统lib已经封装好的urlsafe_b64decode方法,当然我们也可以先转成标准的base64,再decode
'''
# python中有urlsafe_b64decode方法,里面封装了对从-到+,从_到/的转换
# jwt中把=给trim了,需要重新补回来,否则base64解码时会报错incorrect padding
header_padded = header + '=' * (4 - len(header) % 4)
payload_padded = payload + '=' * (4 - len(header) % 4)
print('padded header:', header_padded)
print('padded payload:', payload_padded)
text_header = base64.urlsafe_b64decode(header_padded)
text_payload = base64.urlsafe_b64decode(payload_padded)
print('header_text:', text_header.decode('utf-8'))
print('payload_text:', text_payload.decode('utf-8'))
print('--------------自行写将urlsafe的base64字符串转化为标准base64编码------------------')
# 自行写代码将urlsafe的base64字符串转化为标准base64编码
standard_base64_str_header = header.replace('-', '+').replace('_', '/') + '=' * (4 - len(header) % 4)
print('header_std:', standard_base64_str_header)
standard_base64_str_payload = payload.replace('-', '+').replace('_', '/') + '=' * (4 - len(payload) % 4)
print('payload_std', standard_base64_str_payload)
print('--------------------------------')
print('--------------行base64解密,在不验签的情况下就可以解密出明文,所以jwt作用不是防泄密,而是防篡改-------------------')
# 进行base64解密,在不验签的情况下就可以解密出明文,所以jwt作用不是防泄密,而是防篡改
text_header = base64.b64decode(standard_base64_str_header.encode('utf-8'))
print('header_text:', text_header.decode('utf-8'))
text_payload = base64.b64decode(standard_base64_str_payload.encode('utf-8'))
print('payload_text:', text_payload.decode('utf-8'))
print('--------------------------------')
print('-----------用公钥验证jwt--------------')
print('1. 用jwt库里封装好的方法解密和验签,如果验证失败,会抛出异常')
rsa_public_key_obj = RSA.importKey(rsa_public_key)
jwt_pub_key = jwk_from_pem(rsa_public_key_obj.export_key())
jwt_pub_key = jwk_from_pem(rsa_public_key)
try:
payload_json = jwt.decode(jwt_token, key=jwt_pub_key, do_time_check=True)
# payload_json = jwt.decode(jwt_token, key=jwt_pub_key, do_time_check=False)
print('验签成功:', payload_json)
except Exception as e:
print('验签失败:', e)
print('2. 自行用RSA公钥进行验证,如果验证失败,结果是False')
from Crypto import Hash
from Crypto.Hash import SHA256
from Crypto.Signature import PKCS1_v1_5
# 用utf-8和ascii都可以,base64中没有双字节的字符,所以encode出来的都是一样的
message = '{}.{}'.format(header, payload)
print('message(header.payload):', message)
print('signature:', signature)
message_bytes = message.encode('ascii')
# signature_bytes = base64.urlsafe_b64decode(signature.encode('ascii') + b'=' * (4 - len(signature) % 4))
signature_bytes = base64.urlsafe_b64decode((signature + '=' * (4 - len(signature) % 4)).encode('ascii'))
rsa_public_key_obj = RSA.importKey(rsa_public_key)
verifier = PKCS1_v1_5.new(rsa_public_key_obj)
message_hash = Hash.SHA256.new(message_bytes)
print('message_hash hex:', message_hash.hexdigest())
verify_result = verifier.verify(message_hash, signature_bytes)
print('RSA验签结果:', verify_result)