zoukankan      html  css  js  c++  java
  • drf JWT认证模块与自定制

    JWT模块

       在djangorestframework中,有一款扩展模块可用于做JWT认证,使用如下命令进行安装:

    pip install djangorestframework-jwt
    

       现在,就让我们开始使用它吧。

    JWT配置

       该模块的所有配置都会从settings.py中进行读取,与drf一样,它会先去读取项目全局文件夹下的settings.py,再去读取自身的settings.py,所以如果我们要对JWT进行配置,则在项目全局文件夹下的settings.py中进行配置即可:

    import datetime
    JWT_AUTH = {
        # 配置过期时间
        'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
        # 配置请求头中携带token的前缀
        'JWT_AUTH_HEADER_PREFIX': 'JWT',
    }
    

       如果你想了解更多配置,则可查看该模块读取的默认配置文件。

    from rest_framework_jwt import settings
    

       在默认配置文件中,你可以看到如下代码,它会先去全局中找配置,再到局部中找配置:

    USER_SETTINGS = getattr(settings, 'JWT_AUTH', None)
    

    auth组件

       下面将介绍如何使用auth组件与JWT配套使用,这当然非常方便,auth组件可以说是Django的核心。

       我打算这样做,对内置的user表做扩展,添加头像字段,只有用户登录后才能修改头像,否则将会采用默认头像。

    准备工作

       首先我们需要对内置的auth_user表做扩展,如下所示:

    from django.db import models
    from django.contrib.auth.models import AbstractUser
    
    class User(AbstractUser):
        avatar = models.FileField(upload_to="avatar",default="avatar/default.png")
    

       其次配置上传文件的路径,声明media所在的位置,以及声明我们对内置的auth_user表做了扩展:

    MEDIA_ROOT = BASE_DIR / "media"
    AUTH_USER_MODEL = "app01.User"
    
    # python manage.py makemigrations
    # python manage.py migrate
    

       最后打开资源暴露接口:

    from django.contrib import admin
    from django.urls import path,re_path
    from django.views.static import serve
    from django.conf import settings
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        re_path(r"^media/(?P<path>.*)", serve, {"document_root": settings.MEDIA_ROOT}),
    ]
    

    注册API

       现在,我们需要来做一个注册的API接口,如下所示:

    class Register(ViewSet):
        def register(self,request,*args,**kwargs):
            serializer = UserModelSerializer(data=request.data)
            if serializer.is_valid():
                serializer.save()
                return Response(data=serializer.data,status=status.HTTP_201_CREATED)
            return Response(data=serializer.errors,status=status.HTTP_401_UNAUTHORIZED)
    

       我们可以规定register这个方法必须是POST请求才能访问,在url中进行配置(ViewSetViewSetMixin的子类,所以有actions参数):

    path('register/', views.Register.as_view(actions={"post":"register"})),
    

       由于auth_user的密码需要密文,所以我们重写了模型序列化器的create方法。

    from rest_framework import serializers
    from rest_framework.exceptions import ValidationError
    
    from app01 import models
    
    class UserModelSerializer(serializers.ModelSerializer):
        re_password = serializers.CharField(required=True, write_only=True)
        # 数据表中不存在该字段,我们自己写一个
    
        class Meta:
            model = models.User
            fields = ("username","password","re_password","email")
            extra_kwargs = {
                "password":{"write_only":True}
            }
    
        def create(self, validated_data):
            password = validated_data.get("password")
            re_password = validated_data.get("re_password")
            email = validated_data.get("email")
    
    
            if re_password != password:
                raise ValidationError("两次密码输入不一致")
    
            if models.User.objects.filter(email=email):
                raise ValidationError("邮箱已被注册")
    
    
            validated_data.pop("re_password")  # 删除即可,然后写入
            user_obj = models.User.objects.create_user(**validated_data)  # 加密创建
            return user_obj
    
    

    签发token

       下面将实现登录接口,如果你使用了auth组件作为扩展那么登录接口将十分的简单。

       JWT模块已经全部帮你完成了,你只需要向下面这么做:

    from rest_framework_jwt.views import obtain_jwt_token  # 导入视图,它都写好了的,并且会做验证
    from rest_framework_jwt.views import ObtainJSONWebToken  # 上面是一个变量,内部实际上是 obtain_jwt_token=ObtainJSONWebToken.as_view()
    
    urlpatterns = [
    	path('login/', obtain_jwt_token),
    	# path('login/', ObtainJSONWebToken.as_view()),
    ]
    
    

       现在,当我们向该接口发送POST请求时,如果校验全部通过,则会发送给我们一个JWT

    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6Inl1bnlhIiwiZXhwIjoxNjA0NTYyMzcyLCJlbWFpbCI6IjIzMjNAcXEuY29tIn0._SmZ0e0mj5QVOKUftAwI3xBX4_BOw1ZNjAi94_U3mXg
    

    JWT认证

       下面我们来实现修改头像,头像必须先登录才能修改,所以要添加JWT认证。

    from rest_framework.permissions import IsAuthenticated  # 导入权限
    from rest_framework_jwt.authentication import JSONWebTokenAuthentication  # 导入认证
    
    class SetAvatar(ViewSet):
    
        authentication_classes = [JSONWebTokenAuthentication]  # 存储到request.user,如果只配置这个,则不登陆也能访问
        permission_classes = [IsAuthenticated]  # 必须已经登陆,即request.user不能是匿名用户
    
        def set_avatar(self,request,*args,**kwargs):
            serializer = UserSetAvatar(instance=request.user,data=request.FILES)
            if serializer.is_valid():
                serializer.save()
                return Response(data="修改成功",status=status.HTTP_205_RESET_CONTENT)
            return Response(data="修改失败",status=status.HTTP_401_UNAUTHORIZED)
    

       序列类如下:

    class UserSetAvatar(serializers.ModelSerializer):
        class Meta:
            model = models.User
            fields = ("avatar",)
            extra_kwargs = {
                "avatar":{"write_only":True},
            }
    

       url配置:

    path('setavatar/', views.SetAvatar.as_view(actions={"post":"set_avatar"})),
    

       现在,我们使用POSTMAN来发送请求,首先要先登录,获得JWT:

       image-20201105155846022

       然后需要在请求头中添加JWT认证,并且在body体中添加新头像:

       需要注意的是在添加JWT认证时,需要在VALUE处添加前缀JWT 随机字符串,以这样的格式提交,这是因为settings.py中设置了前缀。

       image-20201105155937569

       image-20201105155949700

       最后点击send,将会提示我们修改成功。

    全局使用

       要在全局使用JWT认证,方式如下,它将作用于所有视图:

    REST_FRAMEWORK = {
        # 认证模块
        'DEFAULT_AUTHENTICATION_CLASSES': (
            'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        ),
        'DEFAULT_PERMISSION_CLASSES':{
        	'rest_framework.permissions.IsAuthenticated',
        }
    }
    

       如果你想取消某一个视图的认证功能,则添加空列表即可:

    authentication_classes = []  
    permission_classes = [] 
    

       局部使用参见JWT认证中的书写。

    JWT认证通过返回信息定制

       在上面的示例中,我们可以看见在用户登录之后,返回信息只有一个JWT字符串,那么我们可不可以将已登录的用户名字返回呢?也是可以的。

       jwt_response_payload_handler()这个函数就是控制返回格式的,我们可以覆写它然后在settings.py中进行配置。

       如下所示:

    def jwt_response_payload_handler(token, user=None, request=None):
        return {
            'status': 0,
            'msg': 'ok',
            'data': {
                'token': token,
                'user': UserModelSerializers(user).data
            }
        }
    

       在settings.py中进行配置,该项配置是配置在REST_FRAMEWORK中,而不是JWT_AUTH中,一定要注意:

    REST_FRAMEWORK = {
        # 配置自定义登录成功后的返回信息
        'JWT_RESPONSE_PAYLOAD_HANDLER':"utils.jwt_response_payload_handler",
    }
    

       最后登录完成的结果如下:

    {
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6Inl1bnlhIiwiZXhwIjoxNjA0NTY0Njk3LCJlbWFpbCI6IjIzMjNAcXEuY29tIn0.cvmM6LvoVkSQETybss3fVVGZNXT099o8U21tzDvdFe4",
        "username": "yunya"
    }
    

    JWT认证流程源码阅读

       又开始愉快的读源码环节了,那么JWT的源码还是比较简单的,下面一起看一看。

    签发流程

       首先,我们来分析一下为什么只写了下面一个登录接口,甚至都没写视图,就可以完成签发。

    from rest_framework_jwt.views import obtain_jwt_token  # 导入视图,它都写好了的,并且会做验证
    from rest_framework_jwt.views import ObtainJSONWebToken  # 上面是一个变量,内部实际上是 obtain_jwt_token=ObtainJSONWebToken.as_view()
    
    urlpatterns = [
    	path('login/', obtain_jwt_token),
    ]
    

       先看obtain_jwt_token,可以发现这个代码:

    obtain_jwt_token = ObtainJSONWebToken.as_view()
    

       我们发现了一个ObtainJSONWebToken这个类,它会执行as_view()方法,先不管,看看它继承了谁:

       image-20201105165232378

       它继承了JSONWebTokenAPIView,并且该类又继承了APIView,那么这个APIView的视图的源码已经阅读过不下五次了,可以查看之前的文章。我们直接来看关于登录的认证,JSONWebTOkenAPIView中实现了一个post方法:

        def post(self, request, *args, **kwargs):
            serializer = self.get_serializer(data=request.data)  # 可以发现它有一个自带的序列化器
            if serializer.is_valid():  # 直接进行验证
                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)
                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)
    

       现在看到序列化器对吧,序列化器其实在这里:

    class ObtainJSONWebToken(JSONWebTokenAPIView):
        serializer_class = JSONWebTokenSerializer
    

       这个是序列化器的源码,在__init__方法中实现了字段:

    class JSONWebTokenSerializer(Serializer):
    
        def __init__(self, *args, **kwargs):
    
            super(JSONWebTokenSerializer, self).__init__(*args, **kwargs)
            self.fields[self.username_field] = serializers.CharField()  # username字段
            self.fields['password'] = PasswordField(write_only=True)  # password字段
    
        @property
        def username_field(self):
            return get_username_field()
    
        def validate(self, attrs):
            credentials = {
                self.username_field: attrs.get(self.username_field),
                'password': attrs.get('password')
            }
    
            if all(credentials.values()):
                user = authenticate(**credentials)  # 这里是执行认证。
    
                if user:
                    if not user.is_active:
                        msg = _('User account is disabled.')
                        raise serializers.ValidationError(msg)
    
                    payload = jwt_payload_handler(user)  # 拿到荷载信息
    
                    return {
                        'token': jwt_encode_handler(payload),   # 荷载信息放进去,来生成JWT的token字符串
                        '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)
    

       也就是说,在执行JSONWebTOkenAPIViewpost()方法时,会走一次数据库查询,根据提交的用户名和密码来拿到用户对象。并且在序列化器中,会生成token()信息。他们都是配置好的一些函数:

    jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
    jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
    jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
    jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER
    

       (我们可不可以自己写一个验证类来覆盖它,然后用于多端登录呢?手机号,邮箱等都可以登录)

       我们继续来看post()方法:

        def post(self, request, *args, **kwargs):
            serializer = self.get_serializer(data=request.data)  # 可以发现它有一个自带的序列化器
            if serializer.is_valid():  # 直接进行验证
                user = serializer.object.get('user') or request.user # request.user等于已经登录的用户
                token = serializer.object.get('token')  # token等于生成的jwt随机字符串
                response_data = jwt_response_payload_handler(token, user, request)  # 设置返回信息
                response = Response(response_data) 
                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)  # 没验证通过
    

       OK,自动签发的流程已经走完了。

       大概的缕一缕,它自己有序列化器,并且在序列化器中完成了JWT字符串的拼接,最后进行返回。

    验证流程

       现在我们再来看一下,当用户登录后,再次访问的验证流程,最开始肯定走认证:

    authentication_classes = [JSONWebTokenAuthentication] 
    

       我们都知道,在APIView中的dispatch()方法中的initial()方法中,会有下面这三条代码:

    self.perform_authentication(request)
    self.check_permissions(request)
    self.check_throttles(request)
    
    # 详细的这三个代码的执行步骤,尤其是认证,可以查看之前的文章
    

       也就是先走认证,走认证时会统一执行一个叫做authenticators()的方法。我们直接找JSONWebTokenAuthentication中的authenticators()方法即可。

       这个类没有authenticators()这个方法,所以它继承类谁呢?

    class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
    

       所以我们要找的其实是BaseJSONWebTokenAuthentication中的authenticators()方法。

       终于,找到了:

        def authenticate(self, request):
    
            jwt_value = self.get_jwt_value(request)  # 获取jwt字符串,进行解析。也就是[ JWT  字符串 ],这个字符串的内容,排除前缀,感兴趣可以看看
            if jwt_value is None:
                return None  # 如果没有JWT验证字符串,则返回None
    
            try:  # 一系列的异常捕获,均来自于内置的jwt模块
                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()  # 无效令牌
    
            user = self.authenticate_credentials(payload)  # 如果都没出错,则执行这里
    
            return (user, jwt_value)  # 返回user对象,这个会赋值给reque.user,这个会jwt字符串会赋值给request.auth
            
    =================Request.user中关于权限认证的地方
    
            for authenticator in self.authenticators:
                try:
                    user_auth_tuple = authenticator.authenticate(self)  # 这里会返回一个user
                except exceptions.APIException:
                    self._not_authenticated()
                    raise
    
                if user_auth_tuple is not None:
                    self._authenticator = authenticator
                    self.user, self.auth = user_auth_tuple  # 进行赋值 self.user=user ,self.auth = jwt_value
                    return
    

       我们来看看user是怎么弄出来的。它是在BaseJSONWebTokenAuthentication类中定义的:

        def authenticate_credentials(self, payload):
    
            User = get_user_model() # 返回模型!!不是记录对象 ,通过settings.py中的AUTH_USER_MODEL进行获取
            username = jwt_get_username_from_payload(payload)  # 获取payload中的用户名
    
            if not username:  # 如果没有用户名就抛出异常
                msg = _('Invalid payload.')
                raise exceptions.AuthenticationFailed(msg)
    
            try:  # 根据荷载中的用户名,在模型User中试图获取出用户的记录对象,也就是说这里会走数据库查询。
                user = User.objects.get_by_natural_key(username)
            except User.DoesNotExist:  # 如果User这个模型不存在则抛出异常,说明
                msg = _('Invalid signature.')
                raise exceptions.AuthenticationFailed(msg)
     
            if not user.is_active:  # 判断账户是否被禁用
                msg = _('User account is disabled.')
                raise exceptions.AuthenticationFailed(msg)
    
            return user  # 返回user
    

    不用auth组件

       看完了上面的源码分析后,我们再来想一想,如果不用auth模块该怎么办?

       其实也很简单,我们自己造一个jwt然后将token这个随机字符串返回就好了。

       认证的时候我们重写一下认证类就好了,反正都是那一套逻辑。

      

    手动签发

       我们的表中要有username以及password字段,使用JWT模块中生成token的几个函数,自己造就一个token

    # views.py
    
    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 users.models import User
    
    
    class LoginView(APIView):  # 登录的时候签发token
        authentication_classes = []
        
        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)
                token = jwt_encode_handler(payload)
                return CommonResponse('100','登陆成功',data={'token':token})
            else:
                return CommonResponse('101', '登陆失败')
    

       如果你想实现多端登录,手机、用户名、邮箱等都能登录,这里也有一份代码,只不过配合了序列化器使用,比较复杂:

    # 使用用户名,手机号,邮箱,都可以登录#
    # 前端需要传的数据格式
    {
    	"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})  # context参数类似与管道,与序列化器进行连接
            # 2 生成序列化类对象
            # 3 调用序列号对象的is_validad
            login_ser.is_valid(raise_exception=True)
            token=login_ser.context.get('token')  # 从管道中取出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('用户不存在')
    

    JWT验证

       验证的时候,继承BaseAuthentication类,重写一下验证方法:

    # app_auth.py
    
    from users.models import User
    
    class MyJSONWebTokenAuthentication(BaseAuthentication):
    
        def authenticate(self, request):
            jwt_value = get_authorization_header(request)
            if not jwt_value:
                raise AuthenticationFailed('Authorization 字段是必须的')
            try:
                payload = jwt_decode_handler(jwt_value)
            except jwt.ExpiredSignature:
                raise AuthenticationFailed('签名过期')
            except jwt.InvalidTokenError:
                raise AuthenticationFailed('非法用户')
            username = jwt_get_username_from_payload(payload)
            print(username)
            user = User.objects.filter(username=username).first()
            print(user)
    
            return user, jwt_value
    

       然后你的某一个视图必须登录后才可以访问,把他添加到认证中就OK了。

    from users.app_auth import JSONWebTokenAuthentication,MyJSONWebTokenAuthentication
    class OrderView(APIView):
        # authentication_classes = [JSONWebTokenAuthentication]  # 不用默认的了,用我们自己的
        authentication_classes = [MyJSONWebTokenAuthentication]  # 由于不是auth_user表,所以不需要判断是否为匿名用户。
        def get(self,request):
            print(request.user)
            return CommonResponse('100', '成功',{'数据':'测试'})
    

    最后我想说

      1.注意前缀,JWT开头,一个空格,后面是JWTtoken字符串

      2.如果你不用auth组件,则需要手动生成JWTtoken字符串与手动进行校验。这很麻烦,所幸JWT这个模块给我们很多方便之处。

      3.多阅读源码,有好处的

  • 相关阅读:
    linux 常见知识2
    python 数组
    linux 常见知识
    python入门1
    linux 入门的常见命令
    聊天机器人開發好消息!!DIALOGFLOW與微信的天作之合!!
    群发短信软件共冶一爐
    Ultimate Facebook Messenger for Business Guide (Feb 2019)
    Top 5 Business Messaging Announcements at Facebook F8 2019
    Ultimate Guide to WhatsApp for Business 2019
  • 原文地址:https://www.cnblogs.com/Yunya-Cnblogs/p/13932987.html
Copyright © 2011-2022 走看看