一、JWT

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准.该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

image-20210818122829608

1.1、JWT的构成

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

(1)header

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

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256

完整的头部就像下面这样的JSON:

1
2
3
4
{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

1
2
3
4
5
6
7
8
9
# 计算过程
import base64,json
data = {
  'typ': 'JWT',
  'alg': 'HS256'
}

header = base64.b64encode(json.dumps(data).encode()).decode()
print(header)
1
2
# 结果
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

1、为什么使用base64编码: 计算机中的字节共有256个组合,对应就是ascii码。网络上交换数据时,比如说从A地传到B地,往往要经过多个路由设备。不同的设备支持的编码方式不一定相同,为了防止乱码的现象。所以将数据基于base64统一处理成ASCII,这样无论设备默认什么编码,都是兼容ASCII的,所以就不会再出现乱码现象。

2、那么Base64到底是怎样编码的呢?

简单来说,任何一个数据无非可以看作一个比特流,如01000100010011101100111010111100011001010……那么我们取6个比特为一组,计算它的ascii值,得到一个字符,这个字符肯定是可见字符,好,把它对应的字符写出来,再取6个比特,计算…,如此下去,直到最后,就完成了编码。

3、注意:

  1. 标准base64只有64个字符(英文大小写、数字和+、/)以及用作后缀等号;
  2. base64是把3个字节变成4个可打印字符,所以base64编码后的字符串一定能被4整除(不算用作后缀的等号);
  3. 等号一定用作后缀,且数目一定是0个、1个或2个。这是因为如果原文长度不能被3整除,base64要在后面添加\0凑齐3n位。为了正确还原,添加了几个\0就加上几个等号。显然添加等号的数目只能是0、1或2;
  4. 严格来说base64不能算是一种加密,只能说是编码转换。使用base64的初衷。是为了方便把含有不可见字符串的信息用可见字符串表示出来,以便复制粘贴;

(2)playload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

1
2
3
4
5
6
 {
  "sub": "1234567890",
  "exp": "3422335555",
  "name": "yuan",
  "admin": True,
}

然后将其进行base64加密,得到Jwt的第二部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 计算过程
import base64,json
data = {
  "sub": "1234567890",
  "exp": "3422335555",
  "name": "yuan",
  "admin": True,
}

preload = base64.b64encode(json.dumps(data).encode()).decode()
print(preload)
1
2
# 结果
eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJleHAiOiAiMzQyMjMzNTU1NSIsICJuYW1lIjogInl1YW4iLCAiYWRtaW4iOiB0cnVlfQ==

(3)signature(签名)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import base64, json, hashlib

if __name__ == '__main__':
    # 头部
    data = {'typ': 'JWT', 'alg': 'HS256'}
    header = base64.b64encode(json.dumps(data).encode()).decode()

    # 载荷
    data = {
      "sub": "1234567890",
      "exp": "3422335555",
      "name": "yuan",
      "admin": True
    }

    preload = base64.b64encode(json.dumps(data).encode()).decode()

    # 签证
    # from django.conf import settings
    # secret = settings.SECRET_KEY
    secret = 'django-insecure-(_+qtd5edmhm%2rdsg+qc3wi@s_k*3cbk-+k2gpg3@qx)z6r+p'
    sign = f"{header}.{preload}.{secret}"

    hs256 = hashlib.sha256()
    hs256.update(sign.encode())
    signature = hs256.hexdigest()

    jwt = f"f{header}.{preload}.{signature}"
    print(jwt)
1
feyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9.eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJleHAiOiAiMzQyMjMzNTU1NSIsICJuYW1lIjogInl1YW4iLCAiYWRtaW4iOiB0cnVlfQ==.3591fd397f21c641d5d2ab6117ab5fc7da9c0e6fc07ac75652762a8564e447e0

1.2、JWT的优缺点

jwt的优点

  1. 实现分布式的单点登陆非常方便
  2. 数据实际保存在客户端,所以我们可以分担服务端的存储压力
  3. JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。

jwt的缺点

  1. 数据保存在了客户端,我们服务端只认jwt,不识别客户端。
  2. jwt可以设置过期时间,但是因为数据保存在了客户端,所以对于过期时间不好调整。# secret_key轻易不要改,一改所有客户端都要重新登录

1.3、基于DRF使用JWT

1.3.1、安装配置JWT

(1)安装

1
pip install djangorestframework-jwt -i https://mirrors.aliyun.com/pypi/simple/

(2)配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
REST_FRAMEWORK = {
    # 自定义认证
    'DEFAULT_AUTHENTICATION_CLASSES': (
        # jwt认证
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        # session认证
        # 'rest_framework.authentication.SessionAuthentication',
        # 'rest_framework.authentication.BasicAuthentication',
    ),
}
import datetime

JWT_AUTH = {
    # jwt的有效时间
    'JWT_EXPIRATION_DELTA': datetime.timedelta(weeks=1),
}

我们django创建项目的时候,在settings配置文件中直接就给生成了一个serect_key,我们直接可以使用它作为我们jwt的serect_key,其实djangorestframework-jwt默认配置中就使用的它。

1.3.2、手动生成jwt(我们暂时用不到)

Django REST framework JWT 扩展的说明文档中提供了手动签发JWT的方法

1
2
3
4
5
6
7
from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER # 生成载荷的函数
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER   # 生成token的函数

payload = jwt_payload_handler(user) # user用户模型对象
token = jwt_encode_handler(payload) # 生成jwt的token(令牌,代表的就是一段字符串)

在用户注册或登录成功后,在序列化器中返回用户信息以后同时返回token即可。

1.3.3、后端生成token

Django REST framework JWT提供了登录获取token的视图,可以直接给这视图指定url路由即可使用。

在users子应用路由urls.py中

1
2
3
4
5
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    path('login/', obtain_jwt_token),
]

在主路由中,引入当前子应用的路由文件

1
2
3
4
urlpatterns = [
        ...
    path('user/', include("users.urls")),
]

接下来,我们可以通过postman来测试下功能,但是jwt是通过username和password来进行登录认证处理的,所以我们要给真实数据,jwt会去我们配置的user表中去查询用户数据的。

image-20210818113036056

得到的载荷信息,我们可以通过js内置的base64编码函数来读取里面内容。举例代码:

1
2
3
4
5
6
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InJvb3QiLCJleHAiOjE2MjcxMTkzODQsImVtYWlsIjoiIn0.Xz_QJ5BPSOsjIB-EymwHaptgG-v1Ic8Aa0FhYhcEErE"
let data = token.split(".")
let user_info = JSON.parse(atob(data[1]))   

// atob()   // base64解码
// btoa()   // base64编码

1.3.4、客户请求登录获取token并保存到本地

客户端保存token,我们使用的是sessionStorage(临时存储,浏览器关闭自动清除)或者localStorage(永久存储)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// javascript中原生提供的2个保存本地数据的本地存储对象,2者存储的时间不同,但是操作方法一模一样。
// sessionStorage 会话存储,数据保存浏览器中,关闭浏览器以后数据丢失
// localStorage   永久存储,数据保存浏览器中,除非手动删除,否则永不丢失

// 添加/修改数据
sessionStorage.setItem("变量名","变量值")   // 简写: sessionStorage.变量名 = 变量值

// 读取数据
sessionStorage.getItem("变量名")   // 简写: sessionStorage.变量名

// 删除数据
sessionStorage.removeItem("变量名") // 删除指定数据
sessionStorage.clear() // 清空当前域名在浏览器中保存的数据,慎用

1.3.5、token验证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView


class Index(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        return HttpResponse("index...")

image-20210818132754021

注意:

发送请求头:Authorization:jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Inl1YW4iLCJleHAiOjE2Mjk4Njg2MTksImVtYWlsIjoiIn0.2B2gEYuqpN8kRTQiLYdRiIT3Q3FmPVMQovPMJ2JlUx8

token前加上jwt空格