Skip to content

about

常用于在前后端分离/小程序项目中。

jwt

这里单说jwt的使用,无关Django、Django restframework:

# 不同的版本之间,用法可能有些区别,本篇示例都以2.7.0的版本为例
pip install pyjwt==2.7.0

jwt构成

JWT就一段字符串,由三段信息构成的,将这三段信息文本用.拼接一起就构成了Jwt token字符串。就像这样:

eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9.eyJzdWIiOiAicm9vdCIsICJleHAiOiAxNjc1MTU0ODI5LjEwNTczMjcsICJpYXQiOiAxNjc1MTU0NzY5LjEwNTczMjcsICJuYW1lIjogInpoYW5na2FpIiwgInVzZXJfaWQiOiAxLCAiYWRtaW4iOiB0cnVlfQ==.143ed94199b866d622988c109d569da3878397789ca49cb8b23dad5d24ee399d

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

jwt的头部承载两部分信息:

  • typ:声明token类型,这里是jwttyp的值也可以是Bear
  • alg:声明签证的加密算法,通常是直接使用HMAC SHA256算法。

完整的头部类似于下面这样:

python
{
    "typ": "JWT",
    "alg": "HS256"
}

Python代码举例:

python
import time
import json
import base64
import hashlib

# ------------ header ------------
# jwt的第一段的header处理
header_data = {
	"typ": "JWT",  # 小写的jwt也行
	"alg": "HS256"
}
bin_header = json.dumps(header_data).encode("utf8")
print(bin_header)  # b'{"typ": "JWT", "alg": "HS256"}'
# 然后对二进制类型的header进行base64编码
header = base64.b64encode(bin_header)
print(header)  # b'eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9'
# base64编码后得到的也是二进制类型的数据,我们想要得到普通的字符,要经过decode
header = header.decode()
# jwt的第一段header就拿到了
print(header)  # eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9

payload

载荷就是存放有效信息的地方,类似于飞机的货仓一样,这些有效信息分为三部分:

  • 标准声明:标准声明指定jwt实现规范中要求的属性(官方建议但不强制使用,也就是下面的参数可写可不写):
    • iss:jwt签发者,一般可以写服务端的域名。
    • sub:jwt所面向的用户,可写用户的ip、域名之类的。
    • aud:接收jwt的一方是谁,可写用户名。
    • exp:jwt的过期时间(时间戳),这个过期时间必须大于签发时间。
    • nbf:定义在什么时间之后,该jwt才可以使用,例如主播21点才开播,那么用户可以提前获取邀请连接,但该邀请连接在21点前是不能进入房间的。
    • iat:jwt的签发时间(时间戳)。
    • jti:jwt的唯一身份标识,主要用来做为一次性token,从而回避重放攻击。
  • 公共声明:公共声明可以添加任何的公开信息,一般添加用户的相关信息或者其他业务需要的信息,但不建议添加敏感信息,因为该部分在客户端是可以解密的。
  • 私有声明:私有声明是提供者和消费者共同定义的声明,一般不建议存放敏感信息,可以存放的是一些可以在服务端或者客户端通过秘钥进行加密和解密的加密信息,往往采用RSA非对称加密算法。
python
import time
import json
import base64
import hashlib

# ------------ header ------------
# jwt的第一段的header处理
header_data = {
	"typ": "JWT",  # 小写的jwt也行
	"alg": "HS256"
}
bin_header = json.dumps(header_data).encode("utf8")
print(bin_header)  # b'{"typ": "JWT", "alg": "HS256"}'
# 然后对二进制类型的header进行base64编码
header = base64.b64encode(bin_header)
print(header)  # b'eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9'
# base64编码后得到的也是二进制类型的数据,我们想要得到普通的字符,要经过decode
header = header.decode("utf8")
# jwt的第一段header就拿到了
print(header)  # eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9

# ------------ payload ------------
# 第二段的载荷信息来看看怎么处理
iat_time = time.time()  # 拿到当前float类型的时间戳时间,后续用在过期时间中
payload_data = {  # 这个字典里面就可以存放一些自定义的数据了
	"sub": "root",
	"exp": iat_time + 60,  # 例如过期时间60秒
	"iat": iat_time,
	# 下面就是一些自定义的数据了
	"name": "zhangkai",
	"user_id": 1,
	"admin": True
}
# 将原始的字典进行base64位编码,目的是得到jwt的第二部分
payload = base64.b64encode(json.dumps(payload_data).encode("utf8")).decode("utf8")
print(payload)  # eyJzdWIiOiAicm9vdCIsICJleHAiOiAxNjc1MTU0MzMxLjk5NDMzMDQsICJpYXQiOiAxNjc1MTU0MjcxLjk5NDMzMDQsICJuYW1lIjogInpoYW5na2FpIiwgInVzZXJfaWQiOiAxLCAiYWRtaW4iOiB0cnVlfQ==

1832669387249680384.png

signature

python
import time
import json
import base64
import hashlib

# ------------ header ------------
# jwt的第一段的header处理
header_data = {
	"typ": "JWT",  # 小写的jwt也行
	"alg": "HS256"
}
bin_header = json.dumps(header_data).encode("utf8")
print(bin_header)  # b'{"typ": "JWT", "alg": "HS256"}'
# 然后对二进制类型的header进行base64编码
header = base64.b64encode(bin_header)
print(header)  # b'eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9'
# base64编码后得到的也是二进制类型的数据,我们想要得到普通的字符,要经过decode
header = header.decode()
# jwt的第一段header就拿到了
print(header)  # eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9

# ------------ payload ------------
# 第二段的载荷信息来看看怎么处理
iat_time = time.time()  # 拿到当前float类型的时间戳时间,后续用在过期时间中
payload_data = {  # 这个字典里面就可以存放一些自定义的数据了
	"sub": "root",
	"exp": iat_time + 60,  # 例如过期时间60秒
	"iat": iat_time,
	# 下面就是一些自定义的数据了
	"name": "zhangkai",
	"user_id": 1,
	"admin": True
}
# 将原始的字典进行base64位编码,目的是得到jwt的第二部分
payload = base64.b64encode(json.dumps(payload_data).encode("utf8")).decode("utf8")
print(payload)  # eyJzdWIiOiAicm9vdCIsICJleHAiOiAxNjc1MTU0MzMxLjk5NDMzMDQsICJpYXQiOiAxNjc1MTU0MjcxLjk5NDMzMDQsICJuYW1lIjogInpoYW5na2FpIiwgInVzZXJfaWQiOiAxLCAiYWRtaW4iOiB0cnVlfQ==

# ------------ signature ------------
# jwt的第三段signature,就是将前两段处理好的header、payload,再加上一个随机字符串SECRET_KEY,拼接成一个字符,然后用HS256算法对其进行加密
# 首先拿到SECRET_KEY,注意该SECRET_KEY是绝对不能泄露的
SECRET_KEY = 'django-insecure-hbcv-y9ux0&8qhtkgmh1skvw#v7ru%t(z-#chw#9g5x1r3z=$p'
three_data =  header + payload + SECRET_KEY
# 然后对拼接后的字符串进行HS256加密
signature = hashlib.sha256(three_data.encode('utf8')).hexdigest()
# 这个signature就是处理好的jwt的第三段的验签
print(signature)  # 5647b9498e38dc78045d6b345b3ce293f4dc47ce44249cec417c79520587a9c3

# ------------ 最后,生成最终的完整的jwt字符串 ------------
# 将上面三段字符串以'.'进行拼接
jwt = f'{header}.{payload}.{signature}'
# 最终的jwt长这样
print(jwt)  # eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9.eyJzdWIiOiAicm9vdCIsICJleHAiOiAxNjc1MTU0ODI5LjEwNTczMjcsICJpYXQiOiAxNjc1MTU0NzY5LjEwNTczMjcsICJuYW1lIjogInpoYW5na2FpIiwgInVzZXJfaWQiOiAxLCAiYWRtaW4iOiB0cnVlfQ==.143ed94199b866d622988c109d569da3878397789ca49cb8b23dad5d24ee399d

再次强调:SECRET_KEY是保存在服务器端的,jwt的签发生成也是在服务器端的,SECRET_KEY就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个SECRET_KEY, 那就意味着客户端是可以自我签发jwt了。

jwt解密

所谓解密就是前端拿到jwt字符串要提取数据,或者,前端访问后端,携带jwt,后端进行校验该jwt的合法性时,要对jwt进行分割,反向base64解码,然后反序列化,拿到最初的数据,然后进行一系列的校验。

来看下Python的示例:

python
# 还是用上面示例中生成的那个jwt字符串
import time
import json
import base64
import hashlib

jwt = "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9.eyJzdWIiOiAicm9vdCIsICJleHAiOiAxNjc1MTU0ODI5LjEwNTczMjcsICJpYXQiOiAxNjc1MTU0NzY5LjEwNTczMjcsICJuYW1lIjogInpoYW5na2FpIiwgInVzZXJfaWQiOiAxLCAiYWRtaW4iOiB0cnVlfQ==.143ed94199b866d622988c109d569da3878397789ca49cb8b23dad5d24ee399d"
# 1. 根据'.'分割jwt字符串,分为三段
header, payload, signature = jwt.split(".")

# 分析下,我们能解码回来的也就是header和payload,通过base64解码,再反序列化就可以了,而第三段signature是HS256加密的,不能反解
# 而header和payload对我们来说,payload才是重要的,因为我们需要的信息都在payload中存放着,header对我们来说不重要,但我们也看下如何解码和反序列化
header = json.loads(base64.b64decode(header.encode('utf8')))
print(header)  # {'typ': 'JWT', 'alg': 'HS256'}

# 接下来我们演示下如何解码payload信息
payload = json.loads(base64.b64decode(payload.encode('utf8')))
# OK了,拿到了原来的payload字典了
print(payload)  # {'sub': 'root', 'exp': 1675154829.1057327, 'iat': 1675154769.1057327, 'name': 'zhangkai', 'user_id': 1, 'admin': True}

# 比如你想判断是否过期,你就可以这样判断下
current_time = time.time()
# 如果exp的时间戳,大于当前时间戳,那肯定没过期
if payload.get("exp", 0) > current_time:
	print('没过期')
else:
	print('过期了')

# 至于jwt的字符串可能被篡改,比如payload中的数据被篡改,或者整个jwt字符串某个字符被篡改,那么后端拿到jwt,split分割之后,
# 再拼上SECRET_KEY,通过HS256算法加密之后的signature,一定跟原来的signature不一致,这就是说明该jwt不合法
# 如下面的客户端传来的jwt字符串有任何一处被修改,我们在后台校验时,都不会通过
jwt = "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9.eyJzdWIiOiAicm9vdCIsICJleHAiOiAxNjc1MTU0ODI5LjEwNTczMjcsICJpYXQiOiAxNjc1MTU0NzY5LjEwNTczMjcsICJuYW1lIjogInpoYW5na2FpIiwgInVzZXJfaWQiOiAxLCAiYWRtaW4iOiB0cnVlfQ==.143ed94199b866d622988c109d569da3878397789ca49cb8b23dad5d24ee399d"
header, payload, signature = jwt.split(".")
SECRET_KEY = 'django-insecure-hbcv-y9ux0&8qhtkgmh1skvw#v7ru%t(z-#chw#9g5x1r3z=$p'
three_data =  header + payload + SECRET_KEY
new_signature = hashlib.sha256(three_data.encode('utf8')).hexdigest()

if new_signature != signature:
	print('jwt不合法')
else:
	print('合法的jwt')

jwt示例

pip install pyjwt==2.7.0

重点就是加密解密部分的函数,你将来可以将这两个函数集成到Django中间件中:

python
import jwt
import datetime
from jwt import exceptions

JWT_SALT = 'iv%x6xo7l7_u9bf_u!9#g#m*)*=ej@bek5)(@u3kh*72+unjv='


def create_token(payload, timeout=20):
    """
    :param payload:  例如:{'user_id':1,'username':'zhangkai'}用户信息
    :param timeout: token的过期时间,默认20分钟
    :return:
    """
    headers = {
        'typ': 'jwt',
        'alg': 'HS256'
    }
    payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout)
    result = jwt.encode(payload=payload, key=JWT_SALT, algorithm="HS256", headers=headers)
    return result


def parse_payload(token):
    """
    对token进行和发行校验并获取payload
    :param token:
    :return:
    """
    result = {'status': False, 'data': None, 'error': None}
    try:
        verified_payload = jwt.decode(token, JWT_SALT.encode('utf8'), algorithms=["HS256"])
        result['status'] = True
        result['data'] = verified_payload
    except exceptions.ExpiredSignatureError:
        result['error'] = 'token已失效'
    except jwt.DecodeError:
        result['error'] = 'token认证失败'
    except jwt.InvalidTokenError:
        result['error'] = '非法的token'
    return result


if __name__ == '__main__':
    # 生成jwt token
    # token = create_token(payload={"user": "zhangkai", "pwd": "123", "user_id": 1}, timeout=1)
    # print(token)
    token = "eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJ1c2VyIjoiemhhbmdrYWkiLCJwd2QiOiIxMjMiLCJ1c2VyX2lkIjoxLCJleHAiOjE2ODQyMzA0NTd9.NQAJpcnQlSyGfk4o_2WIPYhurcEqx6X3PCZGxkAgxOM"
    # 校验token
    result = parse_payload(token)
    print(result)

jwt在Django中的应用示例(基于jwt原生实现)

无论是在纯Django项目还是drf项目,都是用的是pyjwt完成的,所以我们要提前进行下载。

install

bash
pip install pyjwt

# 我用的是2.7.0的版本
pip install pyjwt==2.7.0

纯Django项目中应用

drf项目中应用

沛奇博客:https://www.cnblogs.com/wupeiqi/p/11854573.html

djangorestframework-jwt模块实现

djangorestframework-jwtpyjwt模块的基础上,再次封装,方便在drf项目中使用。

注意,djangorestframework-jwt模块是结合Django内置的auth组件实现的,也就是说我们要用到Django内置的User表,或者我们也可以扩展User表。

install

pip install djangorestframework-jwt

Django项目settings.py中的配置

python
# drf配置
REST_FRAMEWORK = {
    # 自定义异常处理
    'EXCEPTION_HANDLER': 'luffycityapi.utils.exceptions.exception_handler',
    # 自定义认证
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',  # jwt认证
        'rest_framework.authentication.SessionAuthentication',           # session认证
        'rest_framework.authentication.BasicAuthentication',
    ),
}

import datetime
# jwt认证相关配置项
JWT_AUTH = {
    # 设置jwt的有效期
    # 如果内部站点,例如:运维开发系统,OA,往往配置的access_token有效期基本就是15分钟,30分钟,1~2个小时
    'JWT_EXPIRATION_DELTA': datetime.timedelta(weeks=1), # 一周有效,
}

IsAuthenticated

python
from rest_framework.permissions import IsAuthenticated
class TestJWT(APIView):
    permission_classes = [IsAuthenticated, ]

    def post(self, request):
        print(111, request.user)
        return Response({"CODE": 1000, 'msg': 'testjwt ok'})

然后请求中,就要需要在请求头中携带token值,请求中写法是这样的,注意,JWT后面和具体的token值之间是有一个空格的。

bash
Authorization: JWT <your_token>

Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InJvb3QiLCJleHAiOjE2OTg0MDc5NDEsImVtYWlsIjoicm9vdEBxcS5jb20iLCJhdmF0YXIiOiIiLCJuaWNrbmFtZSI6IiIsIm1vbmV5IjowLjAsImNyZWRpdCI6MH0.uAs8RENH80hMNyWyY2PObkW-Gx0ujvRMF-f08k5gxio

常见报错

Uncaught DOMException: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.

在浏览器的控制台直接使用atob报错:

1832669387962712064.png

那你可以尝试这么做:

javascript
token = "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9.eyJzdWIiOiAicm9vdCIsICJleHAiOiAxNjc1MTU0ODI5LjEwNTczMjcsICJpYXQiOiAxNjc1MTU0NzY5LjEwNTczMjcsICJuYW1lIjogInpoYW5na2FpIiwgInVzZXJfaWQiOiAxLCAiYWRtaW4iOiB0cnVlfQ==.143ed94199b866d622988c109d569da3878397789ca49cb8b23dad5d24ee399d"

atob(token.split(".")[1])
JSON.parse(atob(token.split(".")[1]))

1832669388164038656.png