JWT
基本概念
在用户登录后,我们需要在不同请求之间记录用户的登录状态,常用方式一般有三种:Cookie,Session和Token。
这里我们使用第三种Token令牌方式来实现认证鉴权,采用Json Web Token认证机制(简称:jwt)。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
jwt官网:https://jwt.io/
jwt规范:https://datatracker.ietf.org/doc/html/draft-ietf-oauth-json-web-token
JWT的构成
JWT就一段由三段信息构成的字符串,将这三段信息文本用.
拼接一起就构成的。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
jwtToken = f"{header}.{payload}.{signature}"
header
jwt的头部承载两部分信息:
- typ: type的缩写,声明token的类型,值一般可以是
JWT
或Bear
。 - alg: algorithm的缩写,声明token的第三方部分(签证)的加密算法,通常直接使用
HMAC SHA256
完整的头部就像下面这样的JSON:
{
"typ": "Bear",
"alg": "HS256"
}
然后将头部进行base64.b64urlencode()编码,构成了第一部分头部。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
python代码实现过程:
import base64,json
data = {
'typ': 'JWT',
'alg': 'HS256'
}
header = base64.b64encode(json.dumps(data).encode()).decode()
# 各个语言中都有base64加密解密的功能,所以我们jwt为了安全,需要配合第三段加密
payload
载荷(payload)就是jwt存放有效信息的部分。这个名字像是特指飞机上承载的货仓,这些有效信息包含三种不同类型的数据:
- 标准声明
- 公共声明
- 私有声明
标准声明 (官方提出建议但不强制使用) :
-
iss: jwt签发者
-
sub: jwt所面向的用户
-
aud: 接收jwt的一方
-
exp: jwt的过期时间,这个过期时间必须要大于签发时间
-
nbf: 定义在什么时间之后,该jwt可以正常使用。
-
iat: jwt的签发时间
-
jti: jwt的唯一身份标识,主要用来作为一次性token,往往采用UUID字符串或随机字符串来充当。
以上是JWT规范中提供的7个官方字段,开发者根据自己的业务进行选用。
公共声明:公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端直接可以查看。
私有声明:私有声明是服务端和客户端所共同定义的声明,一般使用类似ace算法进行非对称加密和解密的,意味着该部分信息可以归类为明文信息。
定义一个payload,json格式的数据:
{
"sub": "1234567890", // 时间戳
"exp": "3422335555", // 时间戳
"name": "John Doe",
"admin": true,
}
然后将其进行base64.b64encode() 编码,得到JWT的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
python代码实现过程:
import base64,json
data = {
"sub": "1234567890",
"exp": "3422335555",
"name": "John Doe",
"admin": True,
"info": "232323ssdgerere3335dssss"
}
payload = base64.b64encode(json.dumps(data).encode()).decode()
# 各个语言中都有base64编码和解码,所以我们jwt为了安全,需要配合第三段签证来进行加密保证jwt不会被人篡改。
signature
JWT的第三部分叫签证信息,主要用于辨真伪,防篡改。签证信息使用加密算法生成,公式:
secret_key = "秘钥" # 只保存服务端,不能外泄
signature = SHA256(base64.b64encode(header) + "." +base64.b64encode(payload),secret_key)
python代码实现过程:
import sys, json, base64, time, hmac
if __name__ == '__main__':
# 头部
data = {'typ': 'JWT', 'alg': 'HS256'}
header = base64.b64encode(json.dumps(data).encode()).decode()
# 载荷
data = {"sub": "1234567890", "exp": "3422335555", "name": "John Doe", "admin": True,
"info": "232323ssdgerere3335dssss"}
payload = base64.b64encode(json.dumps(data).encode()).decode()
# 签证,生成jwt token 提供给客户端
# from django.conf import settings
# secret = settings.SECRET_KEY
secret = 'django-insecure-(_+qtd5edmhm%2rdsg+qc3wi@s_k*3cbk-+k2gpg3@qx)z6r+p'
sign = base64.b64encode(f"{header}.{payload}".encode())
signature = base64.b64encode(hmac.digest(secret.encode(), sign, digest="sha256")).decode()
jwt = f"f{header}.{payload}.{signature}"
print(jwt)
# 将这三部分用`.`连接成一个完整的字符串,构成了最终的jwt:
# feyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9.eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJleHAiOiAiMzQyMjMzNTU1NSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImFkbWluIjogdHJ1ZSwgImluZm8iOiAiMjMyMzIzc3NkZ2VyZXJlMzMzNWRzc3NzIn0=.3OnGXAx5wWA5AjxyewICSn5Hirz1tXfzxOc4tns4elM=
注意:
secret是保存在服务器端的,jwt的签发生成代码也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,
所以它应该是服务端的私钥,在任何场景下都不应该流露出去,而且应该在每次服务端更新维护后及时更新。
一旦第三方得知这个secret, 那就意味着他们绕过服务端伪造jwt了。
优缺点
优点:
- 实现分布式集群的单点登陆非常方便
- Token实际保存在客户端,所以我们可以分担服务端的存储压力。
- jwt不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
缺点:
- jwt保存在客户端,我们服务端只认jwt,不识别客户端。
- 解决方案1. 设置客户端唯一登陆
- 解决方案2. 绑定客户端的标记符和IP,机器码
- jwt可以设置过期时间,但是因为jwt保存在了客户端,所以对于过期时间不好调整,一旦签发不可控。
- 解决方案1:设置短有效期,例如:30分、15分钟、10分钟之类。
- 解决方案2:生成jwt的时候,提供给客户端之前先在内存(一般使用内存数据库redis,而不是变量)备份jwt,每次用户访问需要登录身份数据时,把token去内存中验证一样。
使用jwt实现认证流程
所谓的认证流程,实际上就是用户登录的过程。
python代码实现认证流程,代码:
import sys, json, base64, time, hmac
if __name__ == '__main__':
# 模拟客户端提交的token
client_token = "feyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9.eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJleHAiOiAiMzQyMjMzNTU1NSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImFkbWluIjogdHJ1ZSwgImluZm8iOiAiMjMyMzIzc3NkZ2VyZXJlMzMzNWRzc3NzIn0=.3OnGXAx5wWA5AjxyewICSn5Hirz1tXfzxOc4tns4elM="
# 把客户端提交的token分割成三段:头部、载荷、签证
header, payload, signature = client_token.split(".")
# 验证是否过期了,先基于base64,接着使用json解码,提供载荷中的过期时间进行比较
payload_data = json.loads(base64.b64decode(payload.encode()))
exp = int(payload_data.get("exp", 0))
now = int(time.time())
if exp < now:
print("token已经过期!")
sys.exit() # 退出程序,实际开发中,应该时响应代码给客户端,不会继续往下执行了。
secret = "django-insecure-(_+qtd5edmhm%2rdsg+qc3wi@s_k*3cbk-+k2gpg3@qx)z6r+p"
# 与生成token时一样的秘钥和数据,再次生成一个签证
new_signature = hmac.digest(secret.encode(), sign, digest="sha256")
# 拿客户端提交上面的token中的签证进行base64解码得到原始的签证
signature = base64.b64decode(signature)
# 通过compare_digest比较两者是否吻合
if hmac.compare_digest(signature, new_signature):
print("认证通过")
else:
print("认证失败,token被串改!")
基本使用
开发中除非找不到,否则我们可以直接使用第三方已经开源的模块来完成相关的功能。大部分要求使用第三方模块是必须star数量>150。
依赖库安装
# python-jose 用于生成和检验JWT令牌
pip install jwt
pip install python-jose
JWT基本使用
生成一个随机的密钥,用于对JWT令牌进行签名加密的。终端执行命令如下:
openssl rand -hex 32
# eac77e4e9a9a767b792779132e84ea37b1f4c31bec56714607f617a3fbdfbd53
创建JWT需要的相关配置项,settings.py
,代码:
# 加密数据所使用的秘钥[盐值]
SECRET_KEY = "eac77e4e9a9a767b792779132e84ea37b1f4c31bec56714607f617a3fbdfbd53"
# 设定JWT令牌签名算法
ALGORITHM = "HS256"
# 设置令牌过期时间变量(单位:秒)
ACCESS_TOKEN_EXPIRE_MINUTES = 30 * 60
创建JWT工具类,utils.py
,代码:
from typing import Optional
from datetime import timedelta, datetime
import settings
from jose import jwt
import uuid
class JWT(object):
JWTError = jwt.JWTError
ExpiredSignatureError = jwt.ExpiredSignatureError
def create_token(self, data: dict, expire_time: Optional[timedelta] = None):
"""
生成Token
:param data: 需要进行JWT令牌加密的用户信息(解密的时候会用到)
:param expire_time: 令牌有效期,单位:秒
:return: token
"""
now_time = datetime.utcnow()
if expire_time:
expire = now_time + timedelta(seconds=expire_time)
else:
expire = now_time + timedelta(seconds=settings.ACCESS_TOKEN_EXPIRE_TIME)
payload = {
"exp": expire,
"iat": now_time,
"nbf": now_time,
"jti": str(uuid.uuid4())
}
payload.update(data)
token = jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return token
def verify_token(self, token: str) -> dict:
"""
验证token
:param token: 客户端发送过来的token
:return: 返回用户信息
"""
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=settings.ALGORITHM)
return payload
if __name__ == '__main__':
"""密码加密与验证"""
# hashing = Hashing()
# hashed_pwd = hashing.hash("123456")
# print(hashed_pwd) # 加密后要保存到数据库中的哈希串
# # 把原密码和加密后的哈希串进行配对,验证通过则返回结果为True
# ret = hashing.verify("123456", hashed_pwd)
# print(ret)
"""JWT"""
jwt_tool = JWT()
try:
# 正确使用
token = jwt_tool.create_token({'username': 'admin', 'sex': True})
print(token)
data = jwt_tool.verify_token(token)
print(data)
# # 因为Token过期导致验证失败
# token = jwt_tool.create_token({'username': 'admin', 'sex': True}, -300)
# print(token)
# data = jwt_tool.verify_token(token)
# print(data)
# # 因为Token被串改导致验证失败
# token = jwt_tool.create_token({'username': 'admin', 'sex': True})
# print(token)
# data = jwt_tool.verify_token(token[:-1])
# print(data)
except (jwt_tool.ExpiredSignatureError, jwt_tool.JWTError) as e:
print("验证失败,", e)
基于自定义中间件创建JWT中间件实现用户身份认证
async def jwt_middleware(request: Request, call_next):
try:
token: str = request.headers["Authorization"].split()[1]
payload = jwt_took.verify(token)
id: str = payload.get("id")
# 查询数据库,是否存在当前用户
user = await models.User.filter(id=id).first(id)
if user is None:
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials")
request.user = user
except (jwt_tool.ExpiredSignatureError, jwt_tool.JWTError):
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials")
response = await call_next(request)
return response
注册JWT中间件,代码:
app.add_middleware(jwt_middleware)