JsonWebToken:pyjwt使用
jwt官网:https://jwt.io/introduction(搜索jwt)
jwt认证过程:
单点登录是目前广泛使用JWT的一个特性
JSON Web Token structure
Header
Payload
Signature
形如:xxxxx.yyyyy.zzzzz
Header形如:
{
“alg”: “HS256”,
“typ”: “JWT”
}
第1段:Base64Url 加密
Payload形如:
There are three types of claims(声明): registered, public, and private claims.
Registered claims: 有用的可交互的声明,比如: iss (issuer,发行者), exp (expiration time,到期时间), sub (subject,主题), aud (audience,受众), and others.
{
“sub”: “1234567890”,
“name”: “John Doe”,
“admin”: true
}
可以存放如:id、exp(过期时间)等参数
Signature形如:
For example if you want to use the HMAC SHA256 algorithm, the signature will be created in the following way:
HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
secret)
只要加盐的盐不暴露,后端拿到前端传过来的token,后端根据第1和第2段来加盐加密,和前端传过来的第3段对比,相等即验证成功
注意:base64url加密是先做base64加密,然后再将 - 替代 + 及 _ 替代 /
python使用jwt
生成不重复的随机数,可以使用微秒级时间戳生成
time.time()获取的是秒级的时间戳,小数点后保留6位,最多可以保留到微秒级
import time
x = lambda : int(time.time()* 1000*1000)
安装pyjwt
Successfully installed pyjwt-2.1.0
pyjwt官方文档
https://pyjwt.readthedocs.io/en/stable/
pyjwt使用
import jwt
import datetime
from jwt import exceptions
import time
import uuid
# uuid.uuid1()获得的是uuid.UUID对象,盐要转换成str,str长度为36
# uuid.uuid1():基于MAC地址,时间戳,随机数来生成唯一的uuid,可以保证全球范围内的唯一性
def create_token():
SALT = str(uuid.uuid1())
print("盐:", SALT)
globals()["salt"] = SALT
# 构造header
headers = {
'alg': "HS512",
'typ': "JWT"
}
# 构造payload
fmt = "%Y-%m-%d %H:%M:%S"
# 过期时间(北京时间)
# expiration_time_str = (datetime.datetime.now() + datetime.timedelta(minutes=2)) \
# .strftime(fmt)
# 过期时间戳,time.mktime():float
# exp_timestamp = time.mktime(time.strptime(expiration_time_str, fmt))
# now_time = datetime.datetime.strptime(expiration_time_str, fmt)
# print("now_time:", now_time)
# print("exp_timestamp", exp_timestamp)
utc_now_str = (datetime.datetime.utcnow() + datetime.timedelta(minutes=20)).strftime(fmt)
utc_now = datetime.datetime.strptime(utc_now_str, fmt)
payload = {
'sub': "x" + str(int(time.time())),
'exp': utc_now
# 'exp': exp_timestamp
}
print("payload", payload)
result = jwt.encode(headers=headers, payload=payload, key=SALT, algorithm="HS512")
return result
注意:pyjwt的payload中,过期时间必须是utc时间(官方文档中说明需要使用utc),而且直接传入datetime时间对象即可,或者传入时间戳也可以(秒级),然后在使用jwt.decode方法获取payload时,就可以拿到准确的过期时间戳(时间戳:秒级,10位,毫秒级是13位,jwt.decode是获取秒级的时间戳),如果在使用jwt.encode时,使用的是北京时间的过期日期或时间戳,那么jwt.decode获取的时间戳依旧会增加8小时,就与自己原本的需求不相符合了
一般在认证成功后,把jwt生成的token返回给用户,以后用户再次访问时候需要携带token,此时jwt需要对token进行超时及合法性校验。
获取token之后,会按照以下步骤进行校验:
将token分割成 header_segment、payload_segment、crypto_segment 三部分
header_segment, payload_segment, crypto_segment = token.split(".")
对第一部分header_segment进行base64url解密,得到header
对第二部分payload_segment进行base64url解密,得到payload
对第三部分crypto_segment进行base64url解密,得到signature
对第三部分signature部分数据进行合法性校验
拼接前两段密文,即:signing_input
从第一段明文中获取加密算法,默认:HS256
使用 算法+盐 对signing_input 进行加密,将得到的结果和signature密文进行比较。
def check_token(token):
try:
verified_payload = jwt.decode(token, globals()["salt"], algorithms="HS512")
return verified_payload
except:
pass
if __name__ == '__main__':
t = create_token()
print(t)
a = check_token(t)
print(a)
print("过期时间", datetime.datetime.fromtimestamp(a['exp']))
# 结果
# 在2021-07-18 15:36执行,过期时间+20mins,所以是2021-07-18 15:56:21
盐: d916dac1-e79a-11eb-a95c-14f6d8e4b681
payload {'sub': 'x1626593781', 'exp': datetime.datetime(2021, 7, 18, 7, 56, 21)}
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ4MTYyNjU5Mzc4MSIsImV4cCI6MTYyNjU5NDk4MX0.y3a8dfHDyRurCFxPv3bu8407YBJsT02ZoeQIgl_8fo-tM3-2yqfZjiv9XCsEVmnXxCXIPy-j2zv5BJrf5gi3Lw
{'sub': 'x1626593781', 'exp': 1626594981}
过期时间 2021-07-18 15:56:21
完善token的校验
1 超过过期时间,但是校验token的盐没有问题,token也没有被修改过,就会报错:jwt.exceptions.ExpiredSignatureError: Signature has expired(token失效)
2 如果盐有问题,或者盐没问题,但是token被修改过,都会报错:jwt.exceptions.InvalidSignatureError: Signature verification failed(非法token)
tips:pyjwt校验时,如果同时存在上述1和2的错误,即签名不对,并且还过期了,优先提示的是 jwt.exceptions.InvalidSignatureError,签名错误,而非token过期
3 校验的token的格式不对:
3.1 前端传的token是"null",或者空字符串,或者"1.2"等等,只要不满足使用split(".")切割能够拿到3个segment,都会报错:jwt.exceptions.DecodeError: Not enough segments
3.2 前端传的token满足split(".")切割能够拿到3个segment,但是是乱写的,比如:"1.2.3"等等,会报错:jwt.exceptions.DecodeError: Invalid header padding,Invalid crypto padding等等,哪怕是1和2部分格式正确了,但是3不对(后端使用自身知晓的盐进行校验,和前端返回来的使用过盐拿到的3对不上,报错jwt.exceptions.InvalidSignatureError,签名无效)
查看源码,大致有如下的异常:
修改后的token校验:
def check_token(token):
try:
verified_payload = jwt.decode(token, globals()["salt"], algorithms="HS512")
return verified_payload
except exceptions.InvalidSignatureError:
print({"code": "601", "error": "非法token!"})
except exceptions.ExpiredSignatureError:
print({"code": "602", "error": "token失效!"})
except exceptions.DecodeError:
print({"code": "603", "error": "token解析异常!"})
except Exception as e:
print({"code": "604", "error": "token错误!", "msg": str(e)})