drf08--自定义JWT的签发和认证、RBAC权限控制以及admin美化

上节回顾

1 过滤的源码分析
    -视图类中配置类属性:filter_backends = ['过滤类']
    -必须继承他俩ListModelMixin+GenericAPIView
    -ListModelMixin的list方法中执行了self.filter_queryset,视图类
    -GenericAPIView找filter_queryset方法
    for backend in list(self.filter_backends):
       queryset = backend().filter_queryset(self.request, queryset, self)
        return queryset
    
2 分页类的源码分析
    -视图类中配置类属性: pagination_class = 分页类
    -必须继承他俩ListModelMixin+GenericAPIView
    -ListModelMixin的list方法中执行了
    page = self.paginate_queryset(queryset) # 分页
    if page is not None:
       serializer = self.get_serializer(page, many=True)
       return self.get_paginated_response(serializer.data)
    
    -GenericAPIView找paginate_queryset方法
    	# self.paginator配置的分页类的对象
    	self.paginator.paginate_queryset(queryset, self.request, view=self)
    -GenericAPIView找get_paginated_response方法
    	# self.paginator配置的分页类的对象
        self.paginator.get_paginated_response(data)
	-具体的分页类:PageNumberPagination
        -paginate_queryset
        -get_paginated_response
        
3 继承APIView实现分页
    -获取了所有数据
    -分页
    	-实例化得到分页对象
        -调用分页对象的paginate_queryset方法完成分页
        -把要分页的数据序列化
        -把序列化后的数据返回(直接返回和使用get_paginated_response)
        
4 自定义分页(出去面试,面试官问:如何自定义分页)
    -使用当此请求的request对象,取出获取的页码数
    -通过页码数和每页显示多少条,具体的取出当前页码的数据

# 5 面试回答
jwt:json web token    
    是一种前后端的登录认证方式,它分token的签发和认证,
    签发的意思是用户登录成功,生成三段式的token串;
    认证指的是用户访问某个接口,需要携带token串过来,我们完成认证

6 三段:头.载荷.签名,每一段都通过base64编码

7 base64的编码和解码

8 djangorestframework-jwt:好久没维护了,使用djangorestframework-simplejwt(自己读一读)
    
9 快速使用
    -快速签发
    	-在路由中(登录功能有了,基于auth的user表实现的登录)
        path('login/', obtain_jwt_token)
    -快速认证
    	-在视图类中配置
        authentication_classes = [JSONWebTokenAuthentication, ] #drf_jwt
    	permission_classes = [IsAuthenticated] # drf

今日内容

1. 源码分析

1.1 JWT签发的源码分析

# 登录请求,就是发了一个携带用户名密码的post请求---》路由中---》obtain_jwt_token
# ---》ObtainJSONWebToken.as_view()---》视图类ObtainJSONWebToken的post请求

class ObtainJSONWebToken(JSONWebTokenAPIView):
    # 只配置序列化类
    serializer_class = JSONWebTokenSerializer
    
class JSONWebTokenAPIView(APIView):
    def post(self, request, *args, **kwargs):
        # 得到一个序列化类的对象,使用传入的request.data
        serializer = self.get_serializer(data=request.data)
        # 序列化类对象的is_valid,会执行字段自己的校验规则,局部钩子,全局钩子
        if serializer.is_valid():
            # 取出user,取出token
            user = serializer.object.get('user') or request.user
            token = serializer.object.get('token')
            # 执行你在配置文件中配置的函数(自定义返回格式)
            response_data = jwt_response_payload_handler(token, user, request)
            response = Response(response_data)
       return response
    
    
# JSONWebTokenSerializer源码
class JSONWebTokenSerializer(Serializer):

    def __init__(self, *args, **kwargs):  # 序列化字段(username、password)
        super(JSONWebTokenSerializer, self).__init__(*args, **kwargs)
        self.fields[self.username_field] = serializers.CharField()
        self.fields['password'] = PasswordField(write_only=True)
    
	def validate(self, attrs):  # 全局钩子--校验数据
        credentials = {
            self.username_field: attrs.get(self.username_field),
            'password': attrs.get('password')
        }
        # {username:lq, password:lqz12345}
        
        if all(credentials.values()):
            # 通过用户名和密码 认证 auth的user表中的用户
            user = authenticate(**credentials)  
            if user:
                # 通过user得到载荷payload:{user_id:1,username:lqz,..}
                payload = jwt_payload_handler(user)
                return {
                    'token': jwt_encode_handler(payload), # 通过荷载得到token串
                    'user': user
                } 

1.2 JWT认证token的源码分析

# 1 读JSONWebTokenAuthentication的父类BaseJSONWebTokenAuthentication的authenticate
class BaseJSONWebTokenAuthentication(BaseAuthentication):
    
    def authenticate(self, request):
        # 取出前端传入的token串
        jwt_value = self.get_jwt_value(request)
        if jwt_value is None: # 如果前端没有传token串,也会认证通过(执行下一个认证类),故后续必须加上权限类
            return None

        try:
            payload = jwt_decode_handler(jwt_value) # 通过token串获得payload,验签(是否被篡改),检查过期时间
        except jwt.ExpiredSignature:
            msg = _('Signature has expired.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = _('Error decoding signature.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise exceptions.AuthenticationFailed()

        # 通过payload获得当前用户:通过user_id---->auth的user表中获取当前用户
        user = self.authenticate_credentials(payload)

        return (user, jwt_value)

# 2 JSONWebTokenAuthentication的get_jwt_value方法,返回token串
class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
    
    def get_jwt_value(self, request):
        # 取出前端传入的token串,通过空格切分
        auth = get_authorization_header(request).split()  # (jwt,token串)
        auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()

        if not auth: # 如果没有传认证的token,返回None
            if api_settings.JWT_AUTH_COOKIE:
                return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
            return None

        if smart_text(auth[0].lower()) != auth_header_prefix:
            return None
		
        # auth元祖('jwt token串') 数据不正常的情况,抛异常 
        if len(auth) == 1: 
            msg = _('Invalid Authorization header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2: 
            msg = _('Invalid Authorization header. Credentials string '
                    'should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)
        return auth[1]
      
# 源码可知:
    rest_framework_jwt 内置的认证类:JSONWebTokenAuthentication
    如果前端没有传token串,也会认证通过(执行下一个认证类),故做用户登录验证时,后续必须加上权限类

2. JWT内置签发方法--自定义响应格式

# 写一个函数
def jwt_response_payload_handler(token, user=None, request=None):
    return {
        'token':token,
        'username':user.username
    }

# 在配置文件setting中配置
# drf_jwt的配置文件 (drf_jwt有个默认配置文件:from rest_framework_jwt import settings)

JWT_AUTH={
  'JWT_RESPONSE_PAYLOAD_HANDLER':'app01.utils.jwt_response_payload_handler'
}

3. 自定义User表,实现JWT的token签发

# 直接写在view视图中
# 或者写在serializer序列化中(就是作业2)
原理:post请求传递数据,序列化器会做数据校验,校验用户是否存在就可以写在这里

from rest_framework.response import Response
from .models import UserInfo

# 从rest_framework_JWT的配置中(使用字符串的形式), 导入rest_framework_JWT 下的两个方法
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

# 尝试:或者自己直接去rest_framework_JWT去导
from rest_framework_jwt.utils import jwt_payload_handler  # 通过user获取payload 载荷
from rest_framework_jwt.utils import jwt_encode_handler  # 通过payload 载荷获取token

class LoginView(APIView):
    def post(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        user = UserInfo.objects.filter(username=username, password=password).first()
        if user:  
            # 用户名和密码正确,登录成功,签发token
            payload = jwt_payload_handler(user)  # payload至少就含有 user_id、username、exp(过期时间)
            token = jwt_encode_handler(payload)
            return Response({'code': '100', 'msg': '登录成功', 'token': token, 'usrename': user.username})
        else:
            return Response({'code': '101', 'msg': '用户名或密码错误'})

4. 自定义认证类,实现JWT的token认证

from rest_framework.authentication import BaseAuthentication  # 基于drf的认证类来写
from rest_framework import exceptions
import jwt
from .models import UserInfo

from rest_framework_jwt.settings import api_settings
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
# 尝试:或者自己直接去rest_framework_JWT去导
from rest_framework_jwt.utils import jwt_decode_handler  # 通过token串获得payload

class JwtAuthentication(BaseAuthentication):
    def authenticate(self, request):
        # 1.取出前端传入的token串 (自定义前端传入token方式: 'token':token)
        token=request.META.get('HTTP_TOKEN')
        # 2.通过token获得payload
        try:
            payload = jwt_decode_handler(token) # 验签(是否被篡改),检查过期时间
        # except Exception:
        #     raise exceptions.AuthenticationFailed('token认证失败')
        except jwt.ExpiredSignature:
            msg = '签名过期'
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg ='解码错误'
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise exceptions.AuthenticationFailed()
        # 通过payload获得当前用户(从数据库自己的表中拿)
        user=UserInfo.objects.filter(pk=payload['user_id']).first()
        
        # 稍微优化一下,不是每次都去数据库查询当前登录用户,直接临时存在内存中
        # 方式一:临时存取到一个user字典中,但后续使用时注意,request.user是个字典 获取id或者username,要使用字典的方式
        user={'id':payload['user_id'],'username':payload['username']}
        # 方式二:临时存取到UserInfo的对象中,但后续使用时注意,request.user是个UserInfo的普通对象,没办法获取用户其他字段数据
        user=UserInfo(id=payload['user_id'],username=payload['username'])
        # 返回当前用户
        return user,token

5. 多方式登录--自定义签发token

使用的是auth组件的拓展user表

# 使用用户名,手机号,邮箱,都可以登录
# 前端需要传的数据格式
    {
        "username":"lqz/1332323223/33@qq.com",
        "password":"lqz12345"
    }
    
# 视图
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin, ViewSet
from app02 import ser

class Login2View(ViewSet):
    def login(self, request, *args, **kwargs):
        # 1 需要 有个序列化的类
        login_ser = ser.LoginModelSerializer(data=request.data,context={'request':request})
        # 2 生成序列化类对象
        # 3 调用序列号对象的is_validad
        login_ser.is_valid(raise_exception=True)
        token=login_ser.context.get('token')
        # 4 return
        return Response({'status':100,'msg':'登录成功','token':token,'username':login_ser.context.get('username')})
    
# 序列化类
from rest_framework import serializers
from api import models
import re
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.utils import jwt_encode_handler,jwt_payload_handler

class LoginModelSerializer(serializers.ModelSerializer):
    username=serializers.CharField()  # 重新覆盖username字段,数据中它是unique,post请求会认为你是保存数据,故字段局部钩子校验不过,会一直报错
    class Meta:
        model=models.User
        fields=['username','password']

    def validate(self, attrs):
        print(self.context)
        # 在这写逻辑
        username=attrs.get('username') # 用户名有三种方式
        password=attrs.get('password')
        # 通过判断,username数据不同,查询字段不一样
        # 正则匹配,如果是手机号
        if re.match('^1[3-9][0-9]{9}$',username):
            user=models.User.objects.filter(mobile=username).first()
        elif re.match('^.+@.+$',username):  # 邮箱
            user=models.User.objects.filter(email=username).first()
        else:
            user=models.User.objects.filter(username=username).first()
        if user: 
            # 存在用户
            # 校验密码,因为是密文,要用check_password
            if user.check_password(password):
                # 签发token
                payload = jwt_payload_handler(user)  # 把user传入,得到payload
                token = jwt_encode_handler(payload)  # 把payload传入,得到token
                self.context['token']=token
                self.context['username']=user.username
                return attrs
            else:
                raise ValidationError('密码错误')
        else:
            raise ValidationError('用户不存在')

6. JWT的配置参数

# jwt的配置
import datetime

JWT_AUTH={
    # 配置token签发的自定义响应格式
    'JWT_RESPONSE_PAYLOAD_HANDLER':'app01.utils.my_jwt_response_payload_handler', 
    
    # 过期时间,手动配置
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), 
}

7. RBAC权限控制

### 详见分类  Django之CRM 的 01.权限组件之权限控制

# RBAC是基于角色的访问控制(Role-Based Access Control )
    在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。
    这就极大地简化了权限的管理。这样管理都是层级相互依赖的,
    权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。

# rbac权限控制一般都是用在公司内部的管理系统(crm,erp,协同平台)中,(python的django写公司内部项目较多,rbac很重要) 
# CRM:客户关系管理系统(Customer Relationship Management)

# 对客户的普通网站,权限控制就是使用三大认证(认证、权限、频率)



# Django的Auth组件采用的认证规则就是RBAC

# Django的Auth组件--RBAC表分析:
    -用户表:auth_user
    -角色表(部门,组表):auth_group
    -权限表:auth_permission
    
    -角色和权限是多对多---》中间表:auth_group_permissions
    -用户和角色是多对多---》中间表:auth_user_groups
    
    -django的auth多写了一个表,用户和权限的多对多:auth_user_user_permissions
    

# Django的admin组件:后台管理,配合Auth组件,可以快速搭建出一个基于RBAC的后台管理系统   

# 几年前,好多公司内部的管理系统,就是使用django的admin快速搭建的

# admin不好看,第三方美化
    -django 1.x 上很火的Xadmin: 前端基于jq+bootstrap,
    	    2.x 上支持不好了,
            3.x 直接不能用了,而且作者也弃坑了,他又做了一个前后端分离的admin
    
    -simple-ui:djanog 3.x
    
# 前后端分离的admin:Django-Vue-Admin(学长写的)
    https://django-vue-admin.com/

8. admin美化:simple-ui

8.1 simpleui安装

# simpleui开源地址:
    https://simpleui.72wo.com/simpleui/
    Github:https://github.com/newpanjing/simpleui
    码云:https://gitee.com/tompeppa/simpleui
        
# 1.安装django-simpleui
    pip3 install django-simpleui

# 2.注册simpleui
    修改settings.py, 将simpleui加入到INSTALLED_APPS里去,放在第一行,也就是django自带admin的前面。

# 智慧大屏
    https://gitee.com/kevin_chou/dataVIS
    
# 基于JavaScript 的开源可视化图表库
echarts 
    https://echarts.apache.org/examples/zh/index.html
        
hicharts
    https://www.highcharts.com.cn/

8.2 美化设置

# 修改Logo
SIMPLEUI_LOGO = 'https://tvax2.sinaimg.cn/mw690/006WgSkjgy1gqqkgv6ha9j30mn0sbnpd.jpg'

# 隐藏右侧SimpleUI广告链接和使用分析
SIMPLEUI_HOME_INFO = False
SIMPLEUI_ANALYSIS = False

# 设置默认主题,指向主题css文件名。Element-ui风格
SIMPLEUI_DEFAULT_THEME = 'element.css'

# 自定义左侧菜单
SIMPLEUI_CONFIG = {
    'system_keep': False,  # 是否使用系统默认菜单。
    
     # 开启排序和过滤功能, 不填此字段为默认排序和全部显示, 空列表[]为全部不显示.
    'menu_display': ['Simpleui','权限认证', '多级菜单测试','动态菜单测试'],
    
    # 设置是否开启动态菜单, 默认为False. 如果开启, 则会在每次用户登陆时动态展示菜单内容
    # 一般建议关闭
    'dynamic': True, 
    
    'menus': [
        {
        'name': 'Simpleui',
        'icon': 'fas fa-code',
        'url': '/home/'  # 自己的地址(前后端混合项目)
    },
        {
        'app': 'auth',  # 对应的app
        'name': '权限认证',
        'icon': 'fas fa-user-shield',
        'models': [{
            'name': '用户',
            'icon': 'fa fa-user',
            'url': 'auth/user/'
        }]
    },
        {
        'name': '多级菜单测试',
        'icon': 'fa fa-file',
      	# 二级菜单
        'models': [
            {
            'name': 'Baidu',
            'icon': 'far fa-surprise',
            # 第三级菜单 ,
            'models': [
                {
                  'name': '爱奇艺',
                  'url': 'https://www.iqiyi.com/dianshiju/'
                  # 第四级就不支持了,element只支持了3级
                }, 
                {
                 'name': '百度问答',
                 'icon': 'far fa-surprise',
                 'url': 'https://zhidao.baidu.com/'
                }
            ]
        },
            {
            'name': '内网穿透',
            'url': 'https://www.wezoz.com',
            'icon': 'fab fa-github'
        }]
    },
        {
        'name': '动态菜单测试' ,
        'icon': 'fa fa-desktop',
        'models': [{
            'name': time.time(),
            'url': 'http://baidu.com',
            'icon': 'far fa-surprise'
        }]
    }]
}

# 隐藏首页的快捷操作和最近动作
SIMPLEUI_HOME_QUICK = False
SIMPLEUI_HOME_ACTION = False

# 修改左侧菜单首页设置
SIMPLEUI_HOME_PAGE = 'https://www.bing.com'  # 指向页面
SIMPLEUI_HOME_TITLE = '必应欢迎你!'  # 首页标题
SIMPLEUI_HOME_ICON = 'fa fa-code'  # 首页图标

# 设置右上角Home图标跳转链接,会以另外一个窗口打开
SIMPLEUI_INDEX = 'https://www.bing.com'

# 实际应用中后台首页通常是控制面板,自己单独一个页面

# 其它常见配置:

# 离线模式。不填该项或者为False的时候,默认从第三方的cdn获取
SIMPLEUI_STATIC_OFFLINE = False
# 关闭Loading遮罩层
SIMPLEUI_LOADING = False
# 关闭登录界面粒子动画
SIMPLEUI_LOGIN_PARTICLES = False

9. django缓存

# 前端混合开发缓存的使用
    -缓存的位置,通过配置文件来操作(以文件为例)
        CACHES = {
             'default': {
              'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', # 指定缓存使用的引擎
              'LOCATION': '/var/tmp/django_cache', # 指定缓存的路径
              'TIMEOUT':300,   # 缓存超时时间(默认为300秒,None表示永不过期)
              'OPTIONS':{
              'MAX_ENTRIES': 300,  # 最大缓存记录的数量(默认300)
              'CULL_FREQUENCY': 3,  # 缓存到达最大个数之后,剔除缓存个数的比例,即:1/CULL_FREQUENCY(默认3)
              }
             }   
        }

    -缓存的粒度:
    	-全站缓存
        	中间件
            MIDDLEWARE = [
                'django.middleware.cache.UpdateCacheMiddleware',
                ...
                'django.middleware.cache.FetchFromCacheMiddleware',
            ]
            CACHE_MIDDLEWARE_SECONDS=10  # 全站缓存时间
            
        -单页面缓存
        	在视图函数上加装饰器
            from django.views.decorators.cache import cache_page
            @cache_page(5)  # 缓存5s钟
            def test_cache(request):
                import time
                ctime=time.time()
                return render(request,'index.html',context={'ctime':ctime})
        	
        -页面局部缓存
        	{% load cache %}  # 模板语法 过滤器-cache
            {% cache 5 'name' %}  # 5表示5s钟,name是唯一key值
				{{ ctime }}
            {% endcache %}
        	
    
# 前后端分离缓存的使用
    -如何使用
        from django.core.cache import cache
        cache.set('key',value可以是任意数据类型)
        cache.get('key')
    -应用场景:
    	-第一次查询所有图书,你通过多表联查序列化之后的数据,直接缓存起来
        -后续,直接先去缓存查,如果有直接返回,没有,再去连表查,返回之前再缓存

补充

1.内置函数:all()

# all() 函数用于判断给定的可迭代参数 iterable 中的所有元素是否都为 TRUE,如果是返回 True,否则返回 False。

函数等价于:
def all(iterable):
    for element in iterable:
        if not element:
            return False
    return True

# 参数
iterable -- 元组或列表。

# 返回值
如果iterable的所有元素不为0、''、None、False或者iterable为空,all(iterable)返回True,否则返回False;

# 注意:空元组、空列表返回值为True,这里要特别注意。

作业

1. 登录功能--签发token 写在序列化器中

因为反序列化时,有数据校验的功能,故去数据库查是否该用户,就可以写在这里

serializer.py中

from rest_framework import serializers
from rest_framework_jwt.utils import jwt_payload_handler, jwt_encode_handler
from app01.models import User

class JWTSerializer(serializers.Serializer):
    username = serializers.CharField(max_length=32)
    password = serializers.CharField(max_length=32, write_only=True)

    def validate(self, attrs):  # attrs 就是字段校验和局部钩子 校验后的数据
        # 1.从校验后的数据中,获取到username和password
        credentials = {
            'username': attrs.get('username'),
            'password': attrs.get('password'),
        }
        # 2.credentials的值,不能为空
        if all(credentials.values()):
            # 3.去数据库中,查询用户
            user = User.objects.filter(**credentials).first()
            # 4.查询成功,获取载荷payload和token
            if user:
                payload = jwt_payload_handler(user)
                # 5.校验成功,返回校验后的数据(此处返回的是token和user)
                return {
                    'token': jwt_encode_handler(payload),
                    'user': user
                }
            else:
                msg = '登录失败,用户名或密码错误'
                raise serializers.ValidationError(msg)
        else:
            msg = '必须填写:username and "password".'
            raise serializers.ValidationError(msg)

view.py中
from app01.serializer import JWTSerializer
# 基于createapiview,重写post或者create方法都可以
class LoginView2(CreateAPIView):
    serializer_class = JWTSerializer
    
    def create(self, request, *args, **kwargs):
        # 1.实例化 序列化类
        serializer = self.get_serializer(data=request.data)
        # 2.开始序列化的校验数据
        if serializer.is_valid():  # 参数raise_exception,表示返回值不用布尔值,使用抛出异常的形式
            token = serializer.validated_data.get('token')
            user = serializer.validated_data.get('user')
            return Response({'code': '100', 'msg': '登录成功', 'token': token, 'usrename': user.username})
        else:
            return Response({'code': '101', 'msg': serializer.errors})

2. 其他作业

# 1 自定义user表,签发token和认证  写在视图中
    见 day87/drf_jwt_custom
  
# 2 使用simple-ui,修改admin样式,对着文档学习一下

# 3 多方式登录,逻辑写在视图类中
# 4 多方式登录,逻辑写在序列化类中      


# 选做
# 1 了解一下什么是对称加密,什么是非对称加密
    所谓对称,就是采用这种加密方法的双方使用方式用同样的密钥进行加密和解密。
    密钥是控制加密及解密过程的指令。算法是一组规则,规定如何进行加密和解密。
    需要对加密和解密使用相同密钥的加密算法。
    由于其速度快,对称性加密通常在消息发送方需要加密大量数据时使用。对称性加密也称为密钥加密。
    
2 Vue-cli创建vue项目,在pycharm中打开
posted @ 2021-12-16 03:11  Edmond辉仔  阅读(92)  评论(0编辑  收藏  举报