drf-jwt源码分析以及自定义token签发认证、alc和rbac

发布时间 2023-03-22 20:21:40作者: ERROR404Notfound

1.drf-jwt源码执行流程

1.1 签发(登录)

1.代码:
urls.py:
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
    path('login/',obtain_jwt_token),
]

2.我们点进obtain_jwt_token源码:
drf/views.py:
obtain_jwt_token = ObtainJSONWebToken.as_view()
refresh_jwt_token = RefreshJSONWebToken.as_view()
verify_jwt_token = VerifyJSONWebToken.as_view()

3.login需要提交用户名和密码,所以是post请求,我们需要在其父类中找到post方法:
ObtainJSONWebToken>>>JSONWebTokenAPIView,在JSONWebTokenAPIView中找到了post方法:
    def post(self, request, *args, **kwargs):
        # serializer是序列化类的对象
        serializer = self.get_serializer(data=request.data)
			# 校验,如果校验通过:
        if serializer.is_valid():
         # 拿到user和token
            user = serializer.object.get('user') or request.user
            token = serializer.object.get('token')
         # 拿到返回格式,之前我们自定义过token的返回格式。
"""
当我们点击方法:jwt_response_payload_handler(token, user, request),跳转到了rest_framework_jwt:jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER。说明JWT_RESPONSE_PAYLOAD_HANDLER需要在配置中指定返回格式。因此我们在设置中指定:JWT_AUTH = {
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.jwt_response.jwt_response',
}。所以返回格式才能按照我们指定的格式返回。
"""

            response_data = jwt_response_payload_handler(token, user, request)
            response = Response(response_data)        
            return response
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
"""
执行if serializer.is_valid():这句话时就会执行序列化类中的代码,但是如何得到user和token,在序列化类的全局钩子中寻找答案:
"""
4.还是回到drf/views.py中:
obtain_jwt_token = ObtainJSONWebToken.as_view()
refresh_jwt_token = RefreshJSONWebToken.as_view()
verify_jwt_token = VerifyJSONWebToken.as_view()
ObtainJSONWebToken后面跟了as_view()说明这是视图类,点进去:
    class ObtainJSONWebToken(JSONWebTokenAPIView):
        serializer_class = JSONWebTokenSerializer
说明JSONWebTokenSerializer就是序列化类。

5.JSONWebTokenSerializer代码:
class JSONWebTokenSerializer(Serializer):
# 这是一个全局钩子,因为上面没有单个字段的校验规则,所以此时的addr就是{'username':'max','password':'max123'}
    def validate(self, attrs):
        credentials = {
      # 这一步还是拿到了用户名,只不过是绕了一下
            self.username_field: attrs.get(self.username_field),
      # 拿到密码
            'password': attrs.get('password')
        }
		# 必须用户名和密码都幼值才成立
        if all(credentials.values()):
        # auth模块中的,如果用户存在会拿到用户对象
            user = authenticate(**credentials)
            if user:
        # 如果能拿到用户对象,并且用户被锁(is_active默认是1,如果用户被锁则是0)
                if not user.is_active:
                # 如果被锁则提示disabled
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)
					# 通过用户对象拿到荷载
                payload = jwt_payload_handler(user)
					# 通过payload生成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)

上节问题:

token自动签发过程中,为什么在路由中路由没有和相应的视图类对应,却也能进行登陆校验?
urls.py代码:
urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/',obtain_jwt_token)
]
可以看出路由并没有对应视图类而是对应obtain_jwt_token,便可以进行登陆校验。
这是因为向login/发送post请求之后,obtain_jwt_token本身也是一个视图类,在obtain_jwt_token的父父类JSONWebTokenAPIView中找到了post请求,在执行if serializer.is_valid()代码过程中在序列化类JSONWebTokenSerializer中进行了用户名和密码的校验,如果用户名和密码正确签发token。

1.2 认证 (认证类)

1.认证类需要从JSONWebTokenAuthentication中找到authenticate方法。在其父类中找到了authenticate方法。
from rest_framework_jwt.authentication import JSONWebTokenAuthentication

    def authenticate(self, request):
        """
        Returns a two-tuple of `User` and token if a valid signature has been 
        """
        # jwt_value就是token字符串
        jwt_value = self.get_jwt_value(request)
        # 如果token值没传,直接返回None
        if jwt_value is None:
            return None

        try:
        # payload是一个字典:{'user_id': 1, 'username': 'max', 'exp': 1676113688, 'email': ''}
            payload = jwt_decode_handler(jwt_value)
        # 还有几种可能拿不到,分别是:篡改token、token过期了、未知错误
        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()
		# 如果没有错误顺利能拿到用户对象
        user = self.authenticate_credentials(payload)
		# 返回当前登录用户,token
        return (user, jwt_value)
    
2.接下来我们来看刚才的方法get_jwt_value(request)是如何拿到token的,该方法在类JSONWebTokenAuthentication中:
    def get_jwt_value(self, request):
        auth = get_authorization_header(request).split()

3.我们需要找到方法get_authorization_header(request),在BaseAuthentication中找到了该方法:
def get_authorization_header(request):
    # request.META可以拿到get请求头当中的值,结果是个字典。在数据发送到后端时键都变成了'HTTP_前端传入的键',如果拿不到就拿一个空字符串。此时的auth是jwt dfjkdlsjf...
    auth = request.META.get('HTTP_AUTHORIZATION', b'')
    if isinstance(auth, str):
        # Work around django test client oddness
        auth = auth.encode(HTTP_HEADER_ENCODING)
    # 转码然后返回
    return auth

4.继续回到get_jwt_value(request)方法:
    def get_jwt_value(self, request):
        # auth是个被分割列表:[jwt,dfjkdlsjf]
        auth = get_authorization_header(request).split()
        # JWT_AUTH_HEADER_PREFIX就是'JWT',转化成小写'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
			# 如果列表索引0不为jwt返回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)
			# 返回列表索引1,也就是token
        return auth[1]

2.自定义用户表签发和认证

2.1 签发

views.py:
from rest_framework import permissions
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.viewsets import ViewSet
from rest_framework.decorators import action
from .models import Userinfo
from rest_framework_jwt.settings import api_settings
# 生成荷载的方法,我们直接调用drf的
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
# 生成token的方法,我们也调用drf的
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
    
class UserView(ViewSet):
    @action(methods=['POST'],detail=False)
    def login(self,request,*args,**kwargs):
        username = request.data.get('username')
        password = request.data.get('password')
        user = Userinfo.objects.filter(username=username,password=password).first()
        if user:
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)
            return Response({'code':100,'msg':'登录成功','token':token})
        else:
            return Response({'code':101,'msg':'用户名或密码错误'})
        
urls.py:
router = SimpleRouter()
router.register('user', UserView, 'user')  # 此时路由:http://127.0.0.1:8000/api/v1/user/login/

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include(router.urls))
]
通过以上步骤,我们可以自定义办法token:
设置token过期时间:
settings.py:
import datetime

JWT_AUTH = {
    # 过期时间1天
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7)
}

2.2 认证

新建一个认证类authentication.py,在其中写认证类的代码:
authentication.py:
from rest_framework.authentication import BaseAuthentication
from rest_framework_jwt.settings import api_settings
import jwt
from rest_framework.exceptions import AuthenticationFailed
from .models import Userinfo
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER


class JsonWebTokenAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request.META.get('HTTP_TOKEN')  # 前端的格式可以自定义,取的时候在前面加上HTTP_就好,并且键要大写
        if token:
            try:
   				# jwt_decode_handler()方法仅仅是通过token找到payload,内部并没有切割字符串的方法 get_jwt_value(),所以前端在传的时候不需要加jwt和空格。
                payload = jwt_decode_handler(
                    token)  
                print(payload)  # :{'user_id': 1, 'username': 'max', 'exp': 1676113688, 'email': ''}
                user = Userinfo.objects.filter(pk=payload.get('user_id')).first()
                return user, token
            except jwt.ExpiredSignature:
                raise AuthenticationFailed('token过期')
            except jwt.DecodeError:
                raise AuthenticationFailed('token认证失败')
            except jwt.InvalidTokenError:
                raise AuthenticationFailed('token无效')
            except Exception as e:
                raise AuthenticationFailed('未知异常')
        raise AuthenticationFailed('token没有传 认证失败')
        
views.py:
class BookView(ModelViewSet):
    # 手写jwt认证只需要写认证类不用写权限类
    authentication_classes = [JsonWebTokenAuthentication]

    def list(self, request, *args, **kwargs):
        return Response('success')

3.auth_user表密码加密

3.1 手动定义类似token的加密方式:

1.token的加密方式:token由三段构成,第一段声明加密算法和类型,第二段存放有效信息的地方:过期时间、签发时间、用户id、用户名等。第三段是加密后的header和base64加密后的payload。
        
2.我们也可以定义一中类似token的加密方式:改密码分为三段,用两个$连接起来,第一段是密码加密后的密文,第二段是随机生成的盐(不加密),第三段是加密后的原密码和盐连接在一起(中间不加符号),在通过md5加密。
    
3.代码:
views.py:
import uuid
import hashlib

def register_hash(request):
    """
    password:原密码
    res:原密码加密之后的密文
    salt:随机生成的盐(不加密)
    pwd1:res$salt
    pwd_part3:password+salt
    res2:给pwd_part3加密之后的密文
    pwd2:最终密码:pwd1+res2
    """
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        # print(password)  # 123
        md51 = hashlib.md5()
        md51.update(password.encode('utf8'))
        res = md51.hexdigest()
        # print('res',res)  # 202cb962ac59075b964b07152d234b70
        # 随机生成一个盐(不加密)
        salt = str(uuid.uuid4())
        print('salt',salt)
        # 将加密的原密码和不加密的盐组合起来,组成密码的前两部分
        pwd1 = res + '$' + salt  # 202cb962ac59075b964b07152d234b70$44590a73-2602-4f96-a718-972d83fb7ae6 
        # 将不加密的密码和盐组合起来,组成明文
        pwd_part3 = res + salt
        md52 = hashlib.md5()
        md52.update(pwd_part3.encode('utf8'))
        # 原密码和盐组成的明文加密,组成密码的第三部分
        res2 = md52.hexdigest()
			# 最终的密码
        pwd2 = pwd1 + '$' + res2
        print(pwd2)
        User_hash.objects.create(username=username,hash_pwd=pwd2)
        return HttpResponse('注册成功')

    return render(request, 'pwd.html', locals())


def login_view(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        md51 = hashlib.md5()
        md51.update(password.encode('utf8'))
        res = md51.hexdigest()
        user_obj = User_hash.objects.filter(username=username).first()
        if not user_obj:
            return HttpResponse('用户未注册')
        if not res == user_obj.hash_pwd.split('$')[0]:
            return HttpResponse('密码错误')
        salt = user_obj.hash_pwd.split('$')[1]
        pwd_part3 = res + salt
        md52 = hashlib.md5()
        md52.update(pwd_part3.encode('utf8'))
        res2 = md52.hexdigest()
        if not res2 == user_obj.hash_pwd.split('$')[2]:
            return HttpResponse('密码错误')
        return HttpResponse('登陆成功')
    return render(request,'login.html',locals())
	
urls.py:
urlpatterns = [
    path('register/',views.register_hash),
    path('login1/',views.login_view)
]

3.2 利用django自带的方法make_password()和check_password()来编写登录注册

1.make_password()只有一个参数,就是原密码。返回值是加密后的密码,也是django的auth_user表中的用户密码的加密方式:

from django.contrib.auth.hashers import make_password, check_password
from .models import User1
# 用djanngo的make_password方法注册
def register2(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        # 密码加密:
        pwd = make_password(password)
        User1.objects.create(username=username,password=pwd)
        return HttpResponse('注册成功')
    return render(request,'register2.html',locals())

2.check_password()方法用来校验密码,里面有两个参数,第一个是明文密码,第二个参数是密文密码,如果这两个密码匹配那么结果是True,不匹配返回结果是False。
# 用django的check_password方法登陆
def login2(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        real_pwd = User1.objects.filter(username=username).first().password
        is_correct = check_password(password,real_pwd)
        if is_correct:
            return HttpResponse('登陆成功')
    return render(request,'login2.html',locals())
"""
如果超级管理员密码忘记了,可以再创建一个超级管理员,然后将新创建的管理员密码(密文)复制到前一个超级管理员的密码处,这两个管理员就会使用同一个密码。
"""

4.simpleui使用

1.之前公司里,做项目,要使用权限,要快速搭建后台管理,使用djagno的admin直接搭建,django的admin界面不好。所以采用第三方软件。

2.第三方的美化:
	xadmin:作者弃坑了,bootstrap+jq 
	simpleui: vue,界面更好看
    
3.现在阶段,一般前后端分离比较多:django+vue

4.1 使用步骤

1.安装:pip install simpleui
    
2.在app中注册
要注册在最上面
INSTALLED_APPS = [
    'simpleui'
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01',
    'rest_framework',
]
然后当我们登录到admin后台管理就变成了这样:

3.然后我们在models.py中构造以下几张表,并且在admin.py中注册:
models.py:
class Book(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=5, decimal_places=2)
    publish_date = models.DateField()
    publish = models.ForeignKey(to='Publish',to_field='nid',on_delete=models.CASCADE)
    authors=models.ManyToManyField(to='Author')
    def __str__(self):
        return self.name

class Author(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    age = models.IntegerField()
    author_detail = models.OneToOneField(to='AuthorDetail',to_field='nid',unique=True,on_delete=models.CASCADE)

class AuthorDetail(models.Model):
    nid = models.AutoField(primary_key=True)
    telephone = models.BigIntegerField()
    birthday = models.DateField()
    addr = models.CharField(max_length=64)

class Publish(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    city = models.CharField(max_length=32)
    email = models.EmailField()
   
admin.py:
from .models import Book,Publish,AuthorDetail,Author
admin.site.register(Book)
admin.site.register(Publish)
admin.site.register(AuthorDetail)
admin.site.register(Author)
然后我们就可以在admin后台管理页看到这几张表:

4.在apps.py中加入verbose_name = '图书管理系统',就可以将左侧列表中的app名改成自定义的名字:
class App01Config(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'app01'
    verbose_name = '图书管理系统'

5.在models.py中每张表下面加入:
    class Meta:
        verbose_name_plural = '作者表'
在后台管理就可以将表名显示成中文:

然后再数据库加一些数据(可以在admin后台管理加,也可以在pycharm中加),添加好之后可以直接点进去修改,这就完成了对一个图书管理系统增删改查的创建。
"""
DataTimeField字段刚开始默认是英文,如果我们想要把它设置成中文,需要在settings.py中设置:LANGUAGE_CODE = 'zh-hans'。
"""

6.当我们在admin.py中注册好之后,我们在页面上只能看到书名。注册还有一种方式,在admin.py中写一个类,定义哪张表选择显示的字段就继承哪张表,用list_play=('字段名')来定义显示的字段名,但是不能上传多对多的外键字段:

7.还可以在页面上增加按钮:在刚才定义的BookAdmin中继续加内容:
    actions = ['custom_button']  # custom_button不能更改

    def custom_button(self, request, queryset):
        print(queryset)  # queryset就是选中对象的queryset,可以额外做一些操作

    custom_button.short_description = '额外操作'  # 按钮的中文名
    custom_button.type = 'success'  # 设置按钮颜色

8.侧边栏设置,需要在settings.py中进行如下设置:
SIMPLEUI_CONFIG = {
    'system_keep': False,
    'menu_display': ['图书管理', '权限认证', '外链'],  # 开启排序和过滤功能, 不填此字段为默认排序和全部显示, 空列表[] 为全部不显示.
    'dynamic': True,  # 设置是否开启动态菜单, 默认为False. 如果开启, 则会在每次用户登陆时动态展示菜单内容
    'menus': [
        # name要和menu_display中注册的名字保持一致
        {  
            'name': '图书管理',
            'app': 'app01',
            'icon': 'fas fa-code',
            # models继续往下写下面的子目
            'models': [
                {
                    'name': '图书',
                    'icon': 'fa fa-user',
                    'url': '/admin/app01/book/'
                },
                #url只能是自己在urls.py中配置的路由或者是自动生成的路由
                {
                    'name': '出版社',
                    'icon': 'fa fa-user',
                    'url': 'app01/publish/'
                },
                {
                    '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'
                }]
        }
    ]
}

9.其他配置项:
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 # 动作

5.权限控制

5.1 互联网项目:

	alc:访问控制列表,权限放在列表中
	用户表:存储用户信息,和权限表是一对多的关系
	权限表:每个用户拥有的权限
     
比如:
	权限列表:[发视频,发评论,开直播]
	max拥有的权限:[发视频,发评论,开直播]
	jerry拥有的权限:[发视频]

5.2 公司内部项目(python写公司内部项目居多):

1.rbac:是基于角色的访问控制(Role-Based Access Control )在 RBAC  中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
        
2表关系:
	用户表:用户和角色是多对多关系(一个用户可以对应多个角色)
	角色表:类似于公司中的岗位
	权限表:用户表不直接和用户表建立联系,而是和角色表建立联系(某个用户成为了某个角色之后才拥有某项权限)。角色表和权限表是多对多关系。
        
所以描述以上三者关系需要建立5张表:
	用户表、角色表、权限表、用户角色表、角色权限表
    
3.用户和权限不直接建立联系是为了简化流程方便管理,但是也有特殊情况:比如公司人资想要获取拉取代码的权限,但是开发角色拥有的权限不仅仅是拉取代码而且还能操作代码。
如果将开发的角色赋给人资就会导致人资的权限过大。所以角色和权限直接建立联系,产生第6张表:角色权限中间表。
    
4.以图书管理系统为例,目前设置2个用户,一个是root(超级管理员),一个是max(普通用户)。目前想要设置max的权限为查看书籍列表和作者列表,需要首先创建一个组(角色),该组中规定了查看书籍列表和作者列表的权限。

在进入到用户设置列表中:首先取消该用户超级管理员身份(公司内超级管理员数量很有限)。

再登陆用户max,发现系统中只有作者和图书两个选项,并且只能查看:

5.管理员也可以直接设置用户和权限的对应关系:

6.在表中权限、组以及它们的对应关系都在以下6张表中:
auth_user:用户表
auth_group:角色表,组表
auth_permission:权限表
auth_user_groups:用户和角色中间表
auth_group_permissions:角色和权限中间表
auth_user_user_permissions:用户和权限中间表

7.用管理员给用户max通过用户和权限对应关系给max添加了一个权限(不通过角色),该功能也可以叠加到max的权限中,用户max现在有三个功能:查看图书和作者(通过角色添加权限)、查看出版社(通过用户添加权限):