drf之jwt

发布时间 2024-01-04 20:53:20作者: jntmwl

jwt介绍

通过上面的第二部分的发展史,我们可以得知目前我们的校验方式的本质就是给前端发cookie,但是后端不存,这个cookie的本质分成三块:请求头、用户的信息以及充当cookie的唯一码、以及签名,这个签名就是签名两部分的内容加密后的东西,这样后端就不用存储session了,前端发送请求的时候只需要把签名解密,然后跟请求头还有内部的用户信息以及cookie进行对比就能校验,并且安全性也比较高。

概念

在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token(本质就是token)认证机制。

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

构成与工作原理

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

1.header

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

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256
  • 还可能有公司信息

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

{
  'typ': 'JWT',
  'alg': 'HS256'
}

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

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

2.payload(荷载)

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

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

主要的信息如下

-荷载:payload
	-存放有效信息的地方
	-过期时间
	-签发时间
	-用户id
	-用户名字等

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

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

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

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

定义一个payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

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

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

3.signature

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

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

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

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

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

关于签发和核验JWT,我们可以使用Django REST framework JWT扩展来完成。

文档网站:http://getblimp.github.io/django-rest-framework-jwt/

本质原理

jwt认证算法:签发与校验

"""
1)jwt分三段式:头.体.签名 (head.payload.sgin)
2)头和体是可逆加密,让服务器可以反解出user对象;签名是不可逆加密,保证整个token的安全性的
3)头体签名三部分,都是采用json格式的字符串,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息
{
	"company": "公司信息",
	...
}
5)体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间
{
	"user_id": 1,
	...
}
6)签名中的内容时安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码 进行md5加密
{
	"head": "头的加密字符串",
	"payload": "体的加密字符串",
	"secret_key": "安全码"
}
"""

签发:根据登录请求提交来的 账号 + 密码 + 设备信息 签发 token

"""
1)用基本信息存储json字典,采用base64算法加密得到 头字符串
2)用关键信息存储json字典,采用base64算法加密得到 体字符串
3)用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串

账号密码就能根据User表得到user对象,形成的三段字符串用 . 拼接成token返回给前台
"""

校验:根据客户端带token的请求 反解出 user 对象

"""
1)将token按 . 拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理
2)第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间和设备信息都是安全信息,确保token没过期,且时同一设备来的
3)再用 第一段 + 第二段 + 服务器安全码 不可逆md5加密,与第三段 签名字符串 进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法的登录用户
"""

drf项目的jwt认证开发流程(重点)

# 前后端登录认证的方式

# 1 登录,签发token---》登录接口
	1 用户携带用户名,密码到后端
	2 校验用户名密码是否正确,如果正确
    3 签发token,按照jwt逻辑生成三段,返回给前端
# 2 认证token---》认证类
	1 用户访问我们需要登录的接口
    2 携带token过来--》请求头,请求地址。。
    3 后端验证用户携带的token,是否被篡改,是否是伪造的,如果没问题
    4 认证通过,继续后续的逻辑
"""
1)用账号密码访问登录接口,登录接口逻辑中调用 签发token 算法,得到token,返回给客户端,客户端自己存到cookies中

2)校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,所有视图类请求,都会进行认证校验,所以请求带了token,就会反解出user对象,在视图类中用request.user就能访问登录的用户

注:登录接口需要做 认证 + 权限 两个局部禁用
"""

补充base64编码解码

ps:base64编码后,字符长度一定是4的倍数,如果不是,使用 = 补齐, = 不表示数据,不补齐会报错

import base64
import json
dic_info={
  "sub": "1234567890",
  "name": "lqz",
  "admin": True
}
byte_info=json.dumps(dic_info).encode('utf-8')
# base64编码
base64_str=base64.b64encode(byte_info)
print(base64_str)
# base64解码
base64_str='eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogImxxeiIsICJhZG1pbiI6IHRydWV9'
str_url = base64.b64decode(base64_str).decode("utf-8")
print(str_url)

base64 应用场景

'''
1 jwt 使用了base64
2 网络中传输数据,也会经常使用 base64编码
3 网络传输中,有的图片使用base64编码
'''
s='去网上找,比如12306的图片都是用base64加密过的,找到他的地址去掉前面两部分就能得到base64加密后的字符串'
res=base64.b64decode(s)
with open('a.png','wb') as f:
    f.write(res)

drf-jwt安装和简单使用

1.1、drf中借助于第三方:

-djangorestframework-jwt  老,年久失修
-djangorestframework-simplejwt 都在用

1.2、安装

# 下载:
	pip3 install djangorestframework-simplejwt
# 写登录--》人家帮咱们写了--》用户表-->用的auth的user表

1.3、使用

步骤一:安装djangorestframework-simplejwtm模块

步骤二:快速签发token

urls.py

from rest_framework_simplejwt.views import token_obtain_pair, token_verify, token_refresh

urlpatterns = [
    path('login/', token_obtain_pair),
    path('verify/', token_verify),
    path('refresh/', token_refresh),
]

3 配置文件

import datetime

SIMPLE_JWT = {

    # token有效时长

    'ACCESS_TOKEN_LIFETIME': datetime.timedelta(minutes=30),

    # token刷新后的有效时间

    'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=1),

}

4 注册app

INSTALLED_APPS = [

    ...

    'rest_framework_simplejwt',

    ...

]

5 迁移表,创建超级用户

createsuperuser

6 测试登录接口,验证接口,刷新接口

7 双token认证:

access,真正使用的token
refresh:用来更新access
access过期时间很短,过期后,需要重新生成access的token保证token的安全

1.4、总结

	-目前,只要没过期,之前签发的access[token]和后来刷新签发的token都能用

关于双token认证问题

单token:

-用户登录---》签发token--》有过期时间
    	3 minute----》重新登录
        7 天------》7天都不需要登录--->被别人截货到---》不安全

双token

	-用户登录---》签发了两个token---》目前的verify验证接口,只要是它签发的token,都会认证通过
    	access:过期时间短 3分
        refresh:过期时间长7天
        
    -用户正常用:都用access,不用refresh
    -access过会就过期了,一旦过期,就永不了---》通过refresh这个token,调用刷新接口,再签发一个access
    -通过refresh再次签发token这个过程,是不需要登录的,对用户就无感知
    
    -后续再用access这个token发送请求
   
    -好处是:access一旦被别人截取到---》拿着模拟发送请求,只能在有效时间内,很快就会失效
    """
    认证类:不能使用refresh的token
    """

人脸识别

人脸识别---》登录---》登录后还要签发token

	-登录:
        1.用户名密码
        2.手机号验证码
        3.一键登录
        4.扫码登录
        5.人脸登录
 登录成功之后。付款,如果需要大额付款则弹出人脸识别做二次校验

认证

认证类---》局部配置和全局配置

使用步骤:

1、局部配置,必须配合权限类

class BookView(APIView):
     # 局部加:认证类--->带来认证信息,会校验,不带认证信息,不管,需要配合一个权限类使用
        authentication_classes = [JWTAuthentication]
        permission_classes = [IsAuthenticated]  # 权限类,没登录的用户没权限
  1. 前端访问:格式必须如下,放在请求头中
Authorization :Bearer access的token

全局使用---它写的登录,去除了认证

    REST_FRAMEWORK = {
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework_simplejwt.authentication.JWTAuthentication',
        ],
        'DEFAULT_PERMISSION_CLASSES': [
            'rest_framework.permissions.IsAuthenticated',
        ],
    }

定制返回格式

1、我们的目标,定制返回格式

{
    code:100,
    msg:'登录成功',
    username:'lqz',
    access:asdfasdf.asdfasdf.asdfasdf          #短效token
    refresh:asdfas.ere.we						#长效token
}

2、 顺便看了一下荷载内容

-access和refresh是有区分的

3 步骤

 1 写个序列化类,重写validate ,返回什么,前端看到什么
    from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
    from rest_framework_simplejwt.views import TokenObtainPairView
    class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
        # 往荷载中加东西
        @classmethod
        def get_token(cls, user):  # user就是登录成功,查到的用户
            token = super().get_token(user)  # 签发token
            token['name'] = user.username  # 往荷载中加用户名
            return token

        def validate(self, attrs):
            old_data = super().validate(attrs)
            data = {'code': 100,
                    'msg': '登录成功成功',
                    'username': self.user.username,
                    'refresh': old_data['refresh'],
                    'access': old_data['access']
                    }
            return data
 
     2 写类方法:get_token--》返回的token中就是荷载的内容
    
    
      3 配置文件配置
        SIMPLE_JWT = {
        	"TOKEN_OBTAIN_SERIALIZER": "app01.serializer.MyTokenObtainPairSerializer",
        }

多方式登录

用户输入:用户名或手机号或邮箱 +密码 都能登录,签发token
-username:手机号/邮箱/名,password
使用auth的user表

新建项目没问题,如果是老项目,迁移过数据了,按照如下操作
1 删库
2 删除项目中app的迁移文件
3 删除源码中 admin和auth中得迁移记录
4 扩写auth的user表
5 重新迁移

多方式登录接口,编写流程

1、面条版

class UserView(APIView):
    authentication_classes = ()
    permission_classes = ()

    def post(self, request):
        # 1  request取出用户名和密码
        username = request.data.get('username')
        password = request.data.get('password')
        # 2  使用正则判断用用户名是邮箱,手机号还是用户名,分别去查询当前用户
        if re.match(r'^1[3-9][0-9]{9}$', username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(r'^.+@.+$', username):
            user = User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()
        if user and user.check_password(password):
            # 3 校验密码
            # 4 签发token
            refresh = TokenObtainPairSerializer.get_token(user)
            # 5 返回给前端
            return Response({'code': 100, 'msg': '成功', 'access': str(refresh.access_token), 'refresh': str(refresh)})
        else:
            return Response({'code': 101, 'msg': '用户名或密码错误'})

2、升级版

urls.py

urlpatterns = [
    path('login_mul/', views.UserView.as_view()),
]

view.py

from rest_framework.generics import GenericAPIView
from .serializer import LoginSerializer


class UserView(GenericAPIView):
    authentication_classes = ()
    permission_classes = ()
    serializer_class = LoginSerializer
    def post(self, request):
        ser = self.get_serializer(data=request.data)
        if ser.is_valid():  # 会执行字段自己的校验(没有),执行局部钩子(没有),执行全局钩子(写了:校验用户,签发token)
            # context 是视图类和序列化列之间沟通的桥梁
            access = ser.context.get('access')
            refresh = ser.context.get('refresh')
            username = ser.context.get('username')
            return Response({'code': 100, 'msg': '成功', 'username': username, 'access': access, 'refresh': refresh})
        else:
            return Response({'code': 101, 'msg': '用户名或密码错误11'})

序列化类

from rest_framework import serializers
from .models import User
import re
from rest_framework.exceptions import ValidationError

class LoginSerializer(serializers.Serializer):
    username = serializers.CharField()
    password = serializers.CharField()

    # 写全局钩子
    def validate(self, attrs):
        # 校验用户,签发token
        username = attrs.get('username')
        password = attrs.get('password')
        # 2  使用正则判断用用户名是邮箱,手机号还是用户名,分别去查询当前用户
        if re.match(r'^1[3-9][0-9]{9}$', username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(r'^.+@.+$', username):
            user = User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()
        if user and user.check_password(password):
            # 3 校验密码
            # 4 签发token
            refresh = TokenObtainPairSerializer.get_token(user)
            self.context['access'] = str(refresh.access_token)
            self.context['refresh'] = str(refresh)
            self.context['username'] = user.username
            return attrs
        else:
            raise ValidationError('用户名或密码错误')

3、plus版

序列化类

class LoginSerializer(serializers.Serializer):
    username = serializers.CharField()
    password = serializers.CharField()

    # 写全局钩子
    def validate(self, attrs):
        # 校验用户,签发token
        username = attrs.get('username')
        password = attrs.get('password')
        # 2  使用正则判断用用户名是邮箱,手机号还是用户名,分别去查询当前用户
        if re.match(r'^1[3-9][0-9]{9}$', username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(r'^.+@.+$', username):
            user = User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()
        if user and user.check_password(password):
            # 3 校验密码
            # 4 签发token
            refresh = TokenObtainPairSerializer.get_token(user)
            data = {'code': 100,
                    'msg': '登录成功成功',
                    'username': user.username,
                    'refresh':str(refresh),
                    'access': str(refresh.access_token)
                    }
            return data
        else:
            raise ValidationError('用户名或密码错误')

view.py

class UserView(GenericAPIView):
    authentication_classes = ()
    permission_classes = ()
    serializer_class = LoginSerializer

    def post(self, request):
        ser = LoginSerializer(data=request.data)
        if ser.is_valid():  # 会执行字段自己的校验(没有),执行局部钩子(没有),执行全局钩子(写了:校验用户,签发token)
            # ser.validated_data # 字典,校验过后的数据
            return Response(ser.validated_data )
        else:
            return Response({'code': 101, 'msg': '用户名或密码错误11'})