drf,drf-jwt源码,自定义用户表签发和认证,simpleui的使用,权限控制(acl,rbac)

内容回顾

接口文档的编写

word,md编写》存放位置:存放共享文件平台,git上,总之就是可以让其他人能访问到

第三方的接口文档编写平台

公司自己开发,使用开源搭建,Yapi

自动生成接口文档
django+drf:swagger,coreapi
FastAPI:自带自动生成接口文档

接口应该有的东西

接口描述

接口地址

接口请求方式

接口编码格式

接口请求参数(get请求方式,post,pu请求参数)

​ 参数类型
​ 参数是否必填
​ 参数解释

返回数据
json格式示例
重点参数解释

错误码:可以写到全局

cookie,session,token发展史

会话保持

cookie:存再与客户端浏览器的键值对

session:存在于服务器的键值对(文件,内存,数据库)

token:三部分:头.载荷.签名
签发:登录
认证:登陆后才能访问的接口
认证类
中间件
装饰器
别的位置

jwt:json,web,token:前后端认证的机制,token的web形式认证机制

base64

应用场景:
jwt
前后端交互:字符串》base64编码
图片:使用base64编码

django+drf使用jwt

django+drf使用simple-jwt

快速签发

配置一条路由》帮我我们写好了路由

定制返回格式

写一个函数,返回什么,前端就看到什么

配置文件中配置

jwt认证

局部:视图类上
认证了》JSONWebTokenAuthentication

​ 权限类》drf IsAuthenticated

项目全局:settings.py文件

携带token

请求头中:authorization:jwt sadsfasdfasdf.asdfas'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300)df.asdfasdf
    
    
    

jwt配置问题

记住的

# 定制返回格式
JWT_RESPONSE_PAYLOAD_HANDLER
#过期时间
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300)

了解的

#请求值中的前缀字符串
'JWT_AUTH_HEADER_PREFIX': 'JWT',
#token签发和核验时候使用的密钥
'JWT_SECRET_KEY': settings.SECRET_KEY,

今日内容

drf-jwt源码执行流程(了解)

切入点    path('login_jwt/', obtain_jwt_token),
路由匹配成功相当于执行obtain_jwt_token()》
在rest_framework.views.py>
obtain_jwt_token = ObtainJSONWebToken.as_view()>相当于ObtainJSONWebToken.as_view()()

class ObtainJSONWebToken(JSONWebTokenAPIView):
	#序列化类
    serializer_class = JSONWebTokenSerializer
    
    
里面有个post方法在继承的父类中JSONWebTokenAPIView
在第54行左右
    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
		# 这里校验诗句是否合法
        if serializer.is_valid():
			

然后进入到序列化类中JSONWebTokenSerializer
在43行执行全局钩子

    def validate(self, attrs):
        {"username":"lqz","password":"lqz1e2345"}
        credentials = {
            # 这里是动态拿数据库里面用户字段名,其实就相当于username,用来获取传入数据中username对应的数据
            self.username_field: attrs.get(self.username_field),
            # 这个是拿传入的密码
            'password': attrs.get('password')
        }
		# 这里判断username和password是否有值,有值这继续走,没值则立即抛出异常
        if all(credentials.values()):
            # 这里是auth模块的校验用户名是否正确,正确返回用户对象,否则返回None,然后就会抛出异常
            user = authenticate(**credentials)

            if user:
                #这里是判断用户是否被禁止了禁止了也是抛出异常
                if not user.is_active:
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)
				# 这里是获取jwt的载荷
                payload = jwt_payload_handler(user)
				然后返回加密后的token,和用户对象
                return {
                    'token': jwt_encode_handler(payload),
                    'user': user
                }
            else:
                msg = _('Unable to log in with provided credentials.')
                raise serializers.ValidationError(msg)
        else:
            msg = _('Must include "{username_field}" and "password".')
            msg = msg.format(username_field=self.username_field)
            raise serializers.ValidationError(msg)
            
            
在回到JSONWebTokenAPIView中的post方法
    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
		#这里如果校验数据合法继续向下走,如果不合法则返回不合法数据原因
        if serializer.is_valid():
            # 这里从序列化类中校验过的数据拿到用户与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)
            # 这里是为了前后端混合时候使用的把token存到cookien中
            if api_settings.JWT_AUTH_COOKIE:
                expiration = (datetime.utcnow() +
                              api_settings.JWT_EXPIRATION_DELTA)
                response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                    token,
                                    expires=expiration,
                                    httponly=True)
            return response

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    
    

image-20230210183948764

重点:

1.通过user得到的载荷=payload = jwt_payload_handler(user)
2.通过载荷签发token:jwt_encode_handler(payload)

jwt-hs256签名

10365b2d42061d1c29d07b06bb5beb9

jwt模块里面

cab0b1101cf9735f6116b60819690d9

c334407e841b67342d0d17e73b082eb

hmac模块

20f5e73d5fc7544dec3aa06e529b256

2c30ddb908735919eed6541cf9c987b

jwt-hs256验证签名

fb9e840d3da7a6100e0cad02f5f0d87

jwt模块里面

4850c78b59726c75057c2c2fd87f623

bfd2dde923519b6f1241f1e7d3b7929

c334407e841b67342d0d17e73b082eb

b0b15f0a44d8652bfcb6a7798a1ce4b

hmac模块

7174a235efd60af52b15162fc9fd6f8

from _operator import _compare_digest as compare_digest

最后在这里进行比对python2.7以上

8ae4357aecd45f45331b87958397249

了解,国际化

翻译函数,只要做了国际化,方的英文,会翻译成该国语言(配置文件中配置的)
这个国际化很垃圾,不建议使用
from django.utils.translation import ugettext as _
	msg = _('Unable to log in with provided credentials.')

image-20230210102854572

认证类

# JSONWebTokenAuthentication---->父类BaseJSONWebTokenAuthentication----》authenticate方法
    def authenticate(self, request):
        # 前端带在请求头中的token 值
        jwt_value = self.get_jwt_value(request)
        # 如果没有携带token,就不校验了
        if jwt_value is None:
            return None

        try:
            # jwt_value就是token
            # 通过token,得到荷载,中途会出错
            # 出错的原因:
            	-篡改token
                -过期了
                -未知错误
            payload = jwt_decode_handler(jwt_value)
        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()

        # 如果能顺利解开,没有被异常捕获,说明token是可以信任的
        # payload就可以使用,通过payload得到当前登录用户
        user = self.authenticate_credentials(payload)
		# 返回当前登录用户,token
        return (user, jwt_value)
# jwt_value = self.get_jwt_value(request)
    def get_jwt_value(self, request):
        # 拿到了前端请求头中传入的 jwt dasdfasdfasdfa
        # auth=[jwt,asdfasdfasdf]
        auth = get_authorization_header(request).split()
        # 'jwt'
        auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()

        if not auth:
            # 请求头中如果没带,去cookie中取
            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

        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]
    
    
    
    
 # 认证类配置了,如果不传jwt,不会校验,一定配合权限类使用

自定义用户表签发和认证

签发

from rest_framework.exceptions import APIException
from .models import User
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


class UserView(APIView):
    def post(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        user = User.objects.filter(username=username, password=password).first()
        if user:
            payload = jwt_payload_handler(user)
            return Response({
                'code': 100,
                'msg': '登录成功',
                'token': jwt_encode_handler(payload)

            })
        else:
            raise APIException('用户名或密码错误')

认证

from rest_framework.authentication import BaseAuthentication
from rest_framework_jwt.settings import api_settings
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER

import jwt

from django.utils.translation import ugettext as _
from rest_framework.exceptions import APIException
from .models import User


class Myauthor(BaseAuthentication):
    def authenticate(self, request):
        token = request.META.get('HTTP_TOKEN', None)
        if token:

            try:
                payload = jwt_decode_handler(token)
                # user = User.objects.filter(username=payload.get('username'))
                # 可以优化
                user = {
                    'id': payload.get('user_id')
                }
                # 后面使用到了在真正的查
                return (user, token)
            except jwt.ExpiredSignature:
                msg = _('Signature has expired.')
                raise APIException(msg)
            except jwt.DecodeError:
                msg = _('Error decoding signature.')
                raise APIException(msg)
            except jwt.InvalidTokenError:
                raise APIException()

            
        else:
            raise APIException('未携带token')

simpleui的使用

# 之前公司里,做项目,要使用权限,要快速搭建后台管理,使用djagno的admin直接搭建,django的admin界面不好

#第三方的美化:
	-xadmin:作者弃坑了,bootstrap+jq 
    -simpleui: vue,界面更好看
    
    
    
# 现在阶段,一般前后端分离比较多:django+vue
	-带权限的前端端分离的快速开发框架
    -django-vue-admin
    -自己写

使用步骤

安装

pip install django-simpleui

在app中注册

INSTALLED_APPS = [
    'simpleui',  # 一定要在最顶上才生效
    'django.contrib.admin',

调整左侧导航栏

-menu_display对应menus name
    -如果是项目的app,就menus写app
    -菜单可以多级,一般咱们内部app,都是一级
    -可以增加除咱们app外的其它链接---》如果是外部链接,直接写地址,如果是内部链接,跟之前前后端混合项目一样的写法:咱们的案例---》show 的路由
SIMPLEUI_CONFIG = {
    'system_keep': False,
    'menu_display': ['图书管理', '权限认证', '张红测试'],  # 开启排序和过滤功能, 不填此字段为默认排序和全部显示, 空列表[] 为全部不显示.
    'dynamic': True,  # 设置是否开启动态菜单, 默认为False. 如果开启, 则会在每次用户登陆时动态展示菜单内容
    'menus': [
        {
            'name': '图书管理',
            'app': 'app01',
            'icon': 'fas fa-code',
            'models': [
                {
                    'name': '图书',
                    'icon': 'fa fa-user',
                    'url': 'app01/book/'
                },
                {
                    'name': '出版社',
                    'icon': 'fa fa-user',
                    'url': 'app01/publisssh/'
                },
                {
                    'name': '作者',
                    'icon': 'fa fa-user',
                    'url': 'app01/author/'
                },
                {
                    'name': '作者详情',
                    'icon': 'fa fa-user',
                    'url': 'app01/authordetail/'
                },
            ]
        },
        {
            'app': 'auth',
            'name': '权限认证',
            'icon': 'fas fa-user-shield',
            'models': [
                {
                    'name': '用户',
                    'icon': 'fa fa-user',
                    'url': 'auth/user/'
                },
                {
                    'name': '组',
                    'icon': 'fa fa-user',
                    'url': 'auth/group/'
                },
            ]
        },
        {

            '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': '/show/',
                    'icon': 'fab fa-github'
                }]
        }
    ]
}
	
    

内部app,图书管理系统某个链接要展示的字段》在admin.py中》自定义按钮

@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    list_display = ('nid', 'name', 'price', 'publish_date', 'publish')

    # 增加自定义按钮
    actions = ['custom_button']

    def custom_button(self, request, queryset):
        print(queryset)

    custom_button.confirm = '你是否执意要点击这个按钮?'
    # 显示的文本,与django admin一致
    custom_button.short_description = '测试按钮'
    # icon,参考element-ui icon与https://fontawesome.com
    # custom_button.icon = 'fas fa-audio-description'
    # # 指定element-ui的按钮类型,参考https://element.eleme.cn/#/zh-CN/component/button
    custom_button.type = 'danger'
    # # 给按钮追加自定义的颜色
    # custom_button.style = 'color:black;'
    

app名字显示中文,字段名字显示中文

新增,查看修改展示中文,在表模型的字段上加:verbose_name='图书名字',help_text='这里填图书名'
	-app名字中文:apps.py---》verbose_name = '图书管理系统'

    
# 6 其它配置项
	SIMPLEUI_LOGIN_PARTICLES = False  #登录页面动态效果
    SIMPLEUI_LOGO = 'https://avatars2.githubusercontent.com/u/13655483?s=60&v=4'#图标替换
    SIMPLEUI_HOME_INFO = False  #首页右侧github提示
    SIMPLEUI_HOME_QUICK = False #快捷操作
    SIMPLEUI_HOME_ACTION = False # 动作

大屏展示

# 监控大屏展示
	-https://search.gitee.com/?skin=rec&type=repository&q=%E5%B1%95%E7%A4%BA%E5%A4%A7%E5%B1%8F
        
    -就是前后端混合项目,js,css,图片对应好,就可以了

权限控制(acl,rbac)


# 公司内部项目
	-rbac:是基于角色的访问控制(Role-Based Access Control )在 RBAC  中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便
    
    -用户表:用户和角色多对多关系,
    -角色表
    	-一个角色可能有多个权限----》
        	-开发角色:拉取代码,上传代码
        	-财务角色:开工资,招人,开除人
            -
    -权限表:角色和权限是多多多
    	-拉取代码
        -上传代码
        -部署项目
        -开工资
        -招人
        -开除人
        -
    -通过5张表完成rbac控制:用户表,角色表,权限表, 用户角色中间表, 角色权限中间表
    -如果某个人,属于财务角色,单只想要拉取代码权限,不要上传代码权限
    -通过6张表:django的admin----》后台管理就是使用这套权限认证
    	用户表,
        角色表,
        权限表, 
        用户角色中间表, 
        角色权限中间表
        用户和权限中间表
# 互联网项目
	-acl:Access Control List 访问控制列表,权限放在列表中
    -权限:权限表----》 发视频,评论,开直播
    -用户表:用户和权限是一对多
    
    张三:[发视频,]
    李四:[发视频,评论,开直播]
    
    
    
    
    
 
# 演示了 django-admin 的权限控制
	-授予lqz 某个组
    -单独授予权限
    
    
# django -auth--6张表
	auth_user   用户表
	auth_group  角色表,组表
    auth_permission  权限表
    -----------
    auth_user_groups   用户和角色中间表
    auth_group_permissions  角色和权限中间表
	-------------
    auth_user_user_permissions  用户和权限中间表
    
    
    
# java:若依
# go :gin-vue-admin
# python :django-vue-admin

补充

#1 django 的auth  user表,密码是加密的,即便的同样的密码,密文都不一样
	-每次加密,都随机生成一个盐,把盐拼在加密后的串中
# 比如
pbkdf2_sha256$260000$B9ZRmPFpWb3H4kdDDmgYA9$CM3Q/ZfYyXzxwvjZ+HOcdovaJS7681kDsW77hr5fo5o=

明文:lqz12345 
盐:B9ZRmPFpWb3H4kdDDmgYA9

后期来了明文lqz12345


#2 自定义用户表,生成密码用密文
from django.contrib.auth.hashers import make_password



#3  用户表的密码忘了怎么办
	-新增一个用户,把它的密码复制过去
    
    
    
    
    
    
# 4 双token认证

加密密码,与加密码的校验(make_password,check_password)

from rest_framework.exceptions import APIException
from .models import User
from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

from django.contrib.auth.hashers import make_password, check_password
from rest_framework.decorators import action


class UserView(ViewSetMixin, APIView):
    @action(methods=['POST'], detail=False, url_path='login',url_name='login')
    def post(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        user = User.objects.filter(username=username).first()

        # user = User.objects.filter(username=username, password=password).first()
        if user:
            is_right = check_password(password, user.password)
            print(is_right)
            if not is_right:
                raise APIException('用户名或密码错误')
            payload = jwt_payload_handler(user)
            return Response({
                'code': 100,
                'msg': '登录成功',
                'token': jwt_encode_handler(payload)

            })
        else:
            raise APIException('用户名或密码错误')

    @action(methods=['POST'], detail=False)
    def createUser(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        assert username and password, APIException('用户名或密码为空')
        password = make_password(password)
        user = User.objects.create(username=username, password=password)
        return Response({'code': 100, 'msg': '注册成功', 'username': user.username})

双token

顾名思义,就是在登陆操作之后由服务端返回两个token:accessToken和refreshToken,在之后的验证登录态的操作中使用这两个token进行验证,其中accessToken的过期时间相当短,refreshToken的过期时间相对于accessToken而言相当长,且会不断的刷新,每次刷新后的refreshToken都是不同的

双token的有点

通过上面的描述也可以或多或少的看出双token验证机制的优点了:

accessToken的存在,保证了登录态的正常验证,因其过期时间的短暂也保证了帐号的安全性
refreshToekn的存在,保证了用户(即使是非活跃用户)无需在短时间内进行反复的登陆操作来保证登录态的有效性,同时也保证了活跃用户的登录态可以一直存续而不需要进行重新登录,其反复刷新也防止某些不怀好意的人获取refreshToken后对用户帐号进行动手动脚的操作(拒绝NTR.jpg)
posted @ 2023-02-10 22:28  clever-cat  阅读(119)  评论(0编辑  收藏  举报