0

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

 1 year ago
source link: https://www.cnblogs.com/zkz0206/p/17112332.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1.drf-jwt源码执行流程

1.1 签发(登录)

python
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',
}。所以返回格式才能按照我们指定的格式返回。
"""

2987409-20230211184302372-245827729.png

2987409-20230211184314327-1746165094.png
python
            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)

1.2 认证 (认证类)

python
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]

2987409-20230211184149193-1607793281.png

2987409-20230211184205467-1305583716.png

2987409-20230211184220601-1146203.png

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

2.1 签发

python
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:

2987409-20230211184100143-183876819.png

2.2 认证

python
新建一个认证类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的加密方式:

python
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()来编写登录注册

python
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使用

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

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

4.1 使用步骤

python
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后台管理就变成了这样:

2987409-20230211184022025-1301922793.png
python
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后台管理页看到这几张表:

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

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

2987409-20230211183857564-679210514.png

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

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

2987409-20230211183622956-1313926354.png
python
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 互联网项目:

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

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

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

2987409-20230211183522976-1593307183.png

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

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

2987409-20230211183359891-1992151521.png
python
5.管理员也可以直接设置用户和权限的对应关系:

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

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

2987409-20230211183244172-838642112.png

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK