使用 Python 实现 JSON Web Token
前言
涉及到无状态鉴权时,Token
是比较好的验证用户身份的方案,而 Token
如何制作也是一个设计难点。只是一个 Token
去下载模块又增加依赖的负担,于是我去学习了 JWT(JSON Web Token)
的设计方式。
JWT 简介
JWT (JSON Web Token)
是一个定义安全信息传输的公开标准(RFC 7519)。使用JWT
标准设计的信息因为其数字签名,都可被验证和信任。
JWT
使用 HMAC
算法或者一个 RSA
或 ECDSA
加密的公/私钥来进行签名认证。使用 JWT
可以帮助实现无状态鉴权和 OAuth 2.0
的实现,也可以用来传输信息。
JWT 结构
JWT
由三部分组成:
- Header
- Payload
- Signature
分别用点 .
隔开组合,一个 JWT 应该长得像下面这样:
xxxx.yyyy.zzzz
Header
header
内包含两个信息,一般来说都应该是相同的,用来告示自己使用的算法如 HS256
。下面是一个 header
的例子:
{
"alg": "HS256",
"typ": "JWT"
}
然后使用 Base64Url 将这个 JSON
编码。
Payload
Payload 用来装载关于比如用户的信息或其他更多信息,包含三部分:
- Registered claims: 这里是对
Token
的签发的说明,如Token
签发方,Token
签发时间,有效时间等等。可以在 这里 看到更多关于 Registered claims 名词的说明。 - Public claims: 这里可以由签发人随意定义,但是要尽量避开
Registered claims
所用到的专有名词。 - Private claims: 这里可以装载想要使用
Token
分享的信息。
下面是关于 Payload
的例子:
{
"iss": "Avimitin Studio",
"exp": "1600483010",
"user": "Tom",
"admin": false
}
然后使用 Base64Url 将这个 JSON
编码。
Signature
接着使用 HMAC
算法对上面编码的两个 JSON
进行加密:
HMACSHA256(header_b64+"."+payload_b64, secret_key)
组合:
将上面三个部分得到的 base64 使用 .
组合起来,就能获得最终的 JWT 了:
Python 实现
虽然 JWT
已经有成熟的模块了,但是在一些环境中能够原生实现相比起让用户安装依赖会更加合适一些,于是我根据上面标准,使用自带模块实现了 JWT。大概步骤如下:
导入包
制作 Token
将需要以下依赖:
# 加密
import hmac
# 获取时间戳
import time
# base64加密
import base64
# 获取字符
import string
# 比 random 更安全的随机
import secrets
# 加密算法
from hashlib import sha256
base64加密
因为 Token
常用于 URL中传递,普通的 base64 加密中的 \
=
等字符会造成歧义,所以我们需要加工一下:
def _safe_base64_url_encode(text):
# 判断输入
if isinstance(text, str):
text = text.encode("utf-8")
elif isinstance(text, bytes):
pass
else:
raise TypeError("Expected string or bytes but got others")
# 使用urlsafe方法得到无歧义的base64字节
text_b64 = base64.urlsafe_b64encode(text)
# 用replace方法将字节里的 = 去掉
return text_b64.replace(b"=", b"")
生成 Header
def header_generate(self):
# 生成 header 之后传递base64加密后的字节
header = """{"alg": "HS256", "typ": "jwt"}"""
return self._safe_base64_url_encode(header)
生成 Payload
def payload_generate(self, username: str, permission: str):
# 参数名是示例,可以自行调整
exp = round(time.time()) + 50
payload = """{"iss": "Avimitin Studio", "exp": "%d", "user": "%s", "admin": "%s"}""" % (exp, username, permission)
return self._safe_base64_url_encode(payload)
生成加密密钥
加密用的密钥我是用的是一次性密钥的方法,你也可以换成自己熟记的密码串用来解密。
- 一次性随机密码串
def _secret_key_generate(len: int):
current_time = round(time.time())
combine_text = string.ascii_letters + str(current_time)
salt = ""
while len > 0:
salt += secrets.choice(combine_text)
len -= 1
return salt.encode("utf-8")
- 或者直接换成自己熟记的密码(尽量不要明文)
def _secret_key_generate():
with open("config/password.json", "r") as password_file:
return json.loads(password_file)["password"]
- 关于 Secrets 模块的更多说明
算法加密
def encrypt(key, msg):
# new 方法生成一个新的HMAC对象,用digest返回加密后的抽样
sign = hmac.new(key, msg, sha256).digest()
return sign
- 关于HMAC的更多API说明
最终合成
def generate(self, username, permission):
# 生成加密用的密钥
key = self._secret_key_generate(16)
print("加密密钥: " + key.decode("utf-8"))
# 将前两段信息的 base64 用 . 合并起来
message = self.header_generate() + b"." + self.payload_generate(username, permission)
# 用密钥把message加密
signature = self.encrypt(key, message)
# 将加密后获得的抽样 base64 加密
signature_b64 = self._safe_base64_url_encode(signature)
part = [message, signature_b64]
# 最终把所有的base64合并并解码为 string 字符串
return b".".join(part).decode("utf-8")
输出样例
最终程序输出样例:
加密密钥: XWBCv6bT8FXNKV2z
JWT: eyJhbGciOiAiSFMyNTYiLCAidHlwIjogImp3dCJ9.eyJpc3MiOiAiQXZpbWl0aW4gU3R1ZGlvIiwgImV4cCI6ICIxNjAwNDg1Nzc4IiwgInVzZXIiOiAiYXZpbWl0aW4iLCAiYWRtaW4iOiAiVHJ1ZSJ9.Kph2-Qin9Xrd3LwLWX5mOtGiei-1Cp2mtWuhzps3ub0
代码样本:
你可以在我的 GitHub 看到完整的代码演示。