python-JWT(Json Web Token)-pyjwt JWT的引入 JWT加密原理: JWT解密原理: python中使用JWT

传统登录认证流程: 

1. 用户第一次登录时, 生成一个token并返回给前台, 同时将其与用户主键一同存在后台服务器上(数据库或缓存中)
2. 下一次访问需要登录的页面时, 将token一起传入
3. 后台拿着token去数据库或缓存中查找是否存在该token, 存在则认证通过, 否则认证不通过

传统认证的缺点:

1. token存在后台, 增加了存储和读取的开销
2. 当存在多个后台服务器时, 需同步共享token, 比较麻烦

JWT认证流程(解决了传统认证的问题):

1. 用户第一次登录时, 生成一个token, 但后台不存储该token

2. 下一次访问需要登录的页面时, 将token一起传入

3. 后台拿着token进行解析和校验, 若解析成功则认证通过, 否则认证不通过

JWT加密原理:

生成的token分为三个部分: HEADER.PAYLOAD.SIGNATURE, 这三个部分都是可逆算法base64加密后的字符串, 最后用点号(.)拼接.如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1. HEADER

代表了加密算法和token类型, 若不显示指定, 默认为:
{
  "alg": "HS256",
  "typ": "JWT"
}
加密后结果为: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2. PAYLOAD

代表了想要传输的业务信息和token的过期时间(可选), 例如:
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 451154141
}
加密后结果为: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

3. SIGNATURE

JWT的关键, 其规则是将前面两段加密后的密文再加上自定义的盐值一起拼接后, 再通过不可逆算法HS256(具体使用的是HEADER中的算法)进行加密, 最后再对该密文进行可逆算法base64加密

盐值(salt): 指的是加密时加入的自定义的字符串, 最好是随机或者杂乱的字符串, 这样更能够确定加密后字符串的唯一性, django中可以使用settings中的SECRET_KEY

加密后结果为: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT解密原理:

拿到请求中传过来的token后:
1. 按.号拆分token, 拿到三段值
2. 对这三段密文进行base64解密, 从明文中拿到加密算法和业务数据以及过期时间
3. 再次拼接前两段的密文和自定义的盐值(该盐值必须和创建token时的盐值一样), 使用HEADER中的算法进行加密
4. 将加密的结果和拿到的token中的第三段解密的明文进行比较, 若完全一致则说明认证成功, 若不一致则说明token被篡改过, 认证失败

对JWT三段式的思考:

JWT的根本思想就是将业务数据通过不可逆算法加密存储在token中, 那么为什么要搞成三段式这么复杂呢?
直接把业务数据加上盐值, 然后用默认不可逆算法生成一段密文字符串进行传输不就可以了吗?
这样加密时是比较简单, 但是解密时却由于不可逆算法而拿不到其中的业务数据, 所以确实需要再加一段式来单独存储业务数据
JWT又加了一段用来存储加密算法, 能够让使用者自己确定具体使用什么算法进行加密, 增加了可扩展性

python中使用JWT

pyjwt

这是python使用JWT的基础包, 在jwt官网中python语言点赞最多的就是pyjwt, 安装方式为:  pip install pyjwt , 这个包已经把加密和解密的逻辑写好了, 我们只需要传入加密算法/业务数据/盐值即可

在rest_framework中使用pyjwt

定义两个接口, 登录(login)和查看订单(order), 只有登录过的用户才能成功访问查看订单接口, 我们可以在登录接口中若成功登录则返回jwt的加密token, 在订单接口中自定义一个认证类, 在认证类中校验token

1. 编辑urls.py

from django.urls import path
from users import views

urlpatterns = [
    path('login/', views.LoginView.as_view()),
    path('order/', views.OrderView.as_view()),
]

2. 编辑登录和订单视图类

1. 登录成功后, 调用获取token的方法 create_token() , 传入参数为用户信息和token过期时间(单位: 分钟), 默认1分钟

2. 在订单视图类中设置认证类 JWTAuthentication

3. create_token和JWTAuthentication都定义在utils包的JWTAuth.py中

from rest_framework.views import APIView
from rest_framework.response import Response
from utils.JWTAuth import create_token, JWTAuthentication

class
LoginView(APIView): def post(self, request, *args, **kwargs): # 获取用户名密码 name = request.data.get('name') pwd = request.data.get('pwd') # 获取User对象 try: user = models.User.objects.filter(name=name, pwd=pwd).first() except Exception: return Response({'status': 1, 'errmsg': '用户名或密码不正确!'}) # 获取token token = create_token({'id': user.id, 'name': user.name}, 1) # 返回成功响应 return Response({'status': 0, 'token': token}) class OrderView(APIView): authentication_classes = [JWTAuthentication, ] def get(self, request): return Response({'status': 0, 'msg': 'ok'})

3. 编辑JWTAuth.py

import jwt
from jwt import exceptions as JWTException
from django.conf import settings
import datetime
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed

def create_token(payload, timeout=1):
    # 给传过来的业务数据增加一个过期时间限制
    payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout)
    # 定义盐值
    salt = settings.SECRET_KEY
    # 默认不可逆加密算法为HS256
    token = jwt.encode(payload=payload, key=salt)
    return token

class JWTAuthentication(BaseAuthentication):
    def authenticate(self, request):
        # 从url参数中获取token
        token = request.query_params.get('token')
        # 盐值
        salt = settings.SECRET_KEY
        # 解码token
        try:
            payload = jwt.decode(jwt=token, key=salt, verify=True)
        except JWTException.ExpiredSignature:
            raise AuthenticationFailed('token已失效')
        except jwt.DecodeError:
            raise AuthenticationFailed('token认证失败')
        except jwt.InvalidTokenError:
            raise AuthenticationFailed('非法的token')

        return payload.get('name'), token

注意: 设置过期时间时, 一定是在payload段中设置, 且键名固定为'exp', 值为  datetime.datetime.utcnow() + datetime.timedelta(xxxx)