当前位置:网站首页>DRF JWT authentication module and self customization

DRF JWT authentication module and self customization

2020-11-06 01:15:11 Mr. yunya

JWT modular

   stay djangorestframework in , There is an extension module that can be used to do JWT authentication , Use the following command to install :

pip install djangorestframework-jwt

   Now? , Let's start using it .

JWT To configure

   All configuration of the module will be from settings.py Read from , And drf equally , It will read the project global folder first settings.py, And then read your own settings.py, So if we're going to do something about JWT To configure , In the project global folder settings.py You can configure it in :

import datetime
JWT_AUTH = {
    #  Configure expiration time 
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
    #  The configuration request header carries token The prefix of 
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
}

   If you want to know more about the configuration , You can view the default configuration file read by the module .

from rest_framework_jwt import settings

   In the default configuration file , You can see the following code , It will go to the global configuration first , Then go to the local configuration :

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

auth Components

   Here's how to use auth Components and JWT Matching use of , It's very convenient of course ,auth Components can be said to be Django At the heart of .

   I'm going to do this , For the built-in user Table expansion , Add avatar field , Only after the user logs in can the avatar be modified , Otherwise, the default avatar will be used .

preparation

   First of all, we need the built-in auth_user Table expansion , As shown below :

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")

   Secondly, configure the path of uploading files , Statement media Where it is , And declare that we have built-in auth_user The table has been extended :

MEDIA_ROOT = BASE_DIR / "media"
AUTH_USER_MODEL = "app01.User"

# python manage.py makemigrations
# python manage.py migrate

   Finally, open the resource exposure interface :

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}),
]

register API

   Now? , We need to do a registered API Interface , As shown below :

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)

   We can stipulate that register This method has to be POST Request access to , stay url To configure (ViewSet yes ViewSetMixin Subclasses of , So there is actions Parameters ):

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

   because auth_user Your password needs ciphertext , So we rewrite the model serializer's create Method .

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)
    #  The field does not exist in the data table , Let's write one ourselves 

    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(" The two passwords are inconsistent ")

        if models.User.objects.filter(email=email):
            raise ValidationError(" Email has been registered ")


        validated_data.pop("re_password")  #  Delete it , And then write 
        user_obj = models.User.objects.create_user(**validated_data)  #  Encryption creation 
        return user_obj

Issue token

   Next, we will implement the login interface , If you use auth Component as an extension, then the login interface will be very simple .

   JWT The modules have been completed for you , You just have to do this down here :

from rest_framework_jwt.views import obtain_jwt_token  #  Import view , It's all written , And will do verification 
from rest_framework_jwt.views import ObtainJSONWebToken  #  It's a variable , The inside is actually  obtain_jwt_token=ObtainJSONWebToken.as_view()

urlpatterns = [
	path('login/', obtain_jwt_token),
	# path('login/', ObtainJSONWebToken.as_view()),
]

   Now? , When we send POST When asked , If all the checks pass , Will send us a JWT

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6Inl1bnlhIiwiZXhwIjoxNjA0NTYyMzcyLCJlbWFpbCI6IjIzMjNAcXEuY29tIn0._SmZ0e0mj5QVOKUftAwI3xBX4_BOw1ZNjAi94_U3mXg

JWT authentication

   Now let's modify the avatar , The avatar must be logged in before it can be modified , So add JWT authentication .

from rest_framework.permissions import IsAuthenticated  #  Import permissions 
from rest_framework_jwt.authentication import JSONWebTokenAuthentication  #  Import Authentication 

class SetAvatar(ViewSet):

    authentication_classes = [JSONWebTokenAuthentication]  #  Store in request.user, If you only configure this , You can visit without logging in 
    permission_classes = [IsAuthenticated]  #  It has to be logged in , namely request.user Can't be anonymous 

    def set_avatar(self,request,*args,**kwargs):
        serializer = UserSetAvatar(instance=request.user,data=request.FILES)
        if serializer.is_valid():
            serializer.save()
            return Response(data=" Modification successful ",status=status.HTTP_205_RESET_CONTENT)
        return Response(data=" Modification failed ",status=status.HTTP_401_UNAUTHORIZED)

   The sequence classes are as follows :

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

   url To configure :

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

   Now? , We use POSTMAN To send a request , First log in first , get JWT:

   image-20201105155846022

   Then you need to add... To the request header JWT authentication , And in body Add a new avatar to the body :

   Note that you are adding JWT At the time of certification , Need to be in VALUE Add a prefix at JWT Random string , Submit in this format , This is because settings.py The prefix is set in .

   image-20201105155937569

   image-20201105155949700

   Finally, click send, It will prompt us that the modification is successful .

Global use

   Use it globally JWT authentication , The way is as follows , It will work on all views :

REST_FRAMEWORK = {
    #  Authentication module 
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES':{
    	'rest_framework.permissions.IsAuthenticated',
    }
}

   If you want to disable authentication for a view , Then add an empty list :

authentication_classes = []  
permission_classes = [] 

   For local use. See Synonyms at JWT Writing in certification .

JWT Authentication is customized by returning information

   In the example above , We can see that after the user logs in , There is only one return message JWT character string , So can we return the name of the logged in user ? It's OK, too .

   jwt_response_payload_handler() This function controls the return format , We can override it and then we can do it in settings.py To configure .

   As shown below :

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

   stay settings.py To configure , This configuration is configured in REST_FRAMEWORK in , instead of JWT_AUTH in , Be sure to pay attention to :

REST_FRAMEWORK = {
    #  Configure the return information after successful login 
    'JWT_RESPONSE_PAYLOAD_HANDLER':"utils.jwt_response_payload_handler",
}

   The result of login is as follows :

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

JWT Certification process source code reading

   I started reading the source code happily again , that JWT Source code or relatively simple , Let's take a look at .

Issuance process

   First , Let's analyze why we only wrote the following login interface , I didn't even write the view , You can sign and issue .

from rest_framework_jwt.views import obtain_jwt_token  #  Import view , It's all written , And will do verification 
from rest_framework_jwt.views import ObtainJSONWebToken  #  It's a variable , The inside is actually  obtain_jwt_token=ObtainJSONWebToken.as_view()

urlpatterns = [
	path('login/', obtain_jwt_token),
]

   First look at obtain_jwt_token, You can find this code :

obtain_jwt_token = ObtainJSONWebToken.as_view()

   We found a ObtainJSONWebToken This class , It will execute as_view() Method , I don't care , See who it inherits :

   image-20201105165232378

   It inherited JSONWebTokenAPIView, And this class inherits APIView, So this APIView The source code of the view has been read more than five times , You can see the previous article . Let's look directly at the authentication of login ,JSONWebTOkenAPIView In the implementation of a post Method :

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)  #  You can see that it has a built-in serializer 
        if serializer.is_valid():  #  Direct verification 
            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)

   Now you see the serializer, right , The serializer is actually here :

class ObtainJSONWebToken(JSONWebTokenAPIView):
    serializer_class = JSONWebTokenSerializer

   This is the source code of the serializer , stay __init__ Method to implement the field :

class JSONWebTokenSerializer(Serializer):

    def __init__(self, *args, **kwargs):

        super(JSONWebTokenSerializer, self).__init__(*args, **kwargs)
        self.fields[self.username_field] = serializers.CharField()  # username Field 
        self.fields['password'] = PasswordField(write_only=True)  # password Field 

    @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)  #  Here's executing authentication .

            if user:
                if not user.is_active:
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)

                payload = jwt_payload_handler(user)  #  Get the load information 

                return {
                    'token': jwt_encode_handler(payload),   #  Load information is put in , To generate JWT Of token character string 
                    '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)

   in other words , In execution JSONWebTOkenAPIView Of post() When the method is used , Will walk a database query , Get the user object according to the user name and password submitted . And in the serializer , Will generate token() Information . They are all configured functions :

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

   ( Can we write a validation class to override it , Then it's used for multi terminal login ? cell-phone number , Email, etc. can log in )

   Let's continue to look at post() Method :

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)  #  You can see that it has a built-in serializer 
        if serializer.is_valid():  #  Direct verification 
            user = serializer.object.get('user') or request.user # request.user Equal to the user who has logged in 
            token = serializer.object.get('token')  # token Equal to generated jwt Random string 
            response_data = jwt_response_payload_handler(token, user, request)  #  Set return information 
            response = Response(response_data) 
            if api_settings.JWT_AUTH_COOKIE:  #  Not set , Don't go 
                expiration = (datetime.utcnow() +
                              api_settings.JWT_EXPIRATION_DELTA)
                response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                    token,
                                    expires=expiration,
                                    httponly=True)
            return response  #  Go straight back to 

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)  #  Failed to verify 

   OK, The automatic sign off process is over .

   Maybe a wisp of , It has its own serializer , And it's done in the serializer JWT String splicing , Finally, go back .

Validation process

   Now let's take a look at , When the user logs in , The verification process for revisiting , At the beginning, I must take certification :

authentication_classes = [JSONWebTokenAuthentication] 

   We all know , stay APIView Medium dispatch() Methods initial() In the method , There will be the following three codes :

self.perform_authentication(request)
self.check_permissions(request)
self.check_throttles(request)

#  Detailed execution steps of these three codes , Especially certification , You can see the previous article 

   That's authentication first , When you go through authentication, you will execute a unified method called authenticators() Methods . Let's go straight to JSONWebTokenAuthentication Medium authenticators() The method can .

   This class doesn't have authenticators() This method , So who does it inherit from ?

class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):

   So what we're looking for is actually BaseJSONWebTokenAuthentication Medium authenticators() Method .

   finally , eureka :

    def authenticate(self, request):

        jwt_value = self.get_jwt_value(request)  #  obtain jwt character string , To analyze . That is to say [ JWT   character string  ], The content of this string , Exclude prefix , If you are interested, you can have a look at 
        if jwt_value is None:
            return None  #  without JWT Validation string , Then return to None

        try:  #  A series of exception catches , All come from the built-in jwt modular 
            payload = jwt_decode_handler(jwt_value)  #  Decode the load part 
        except jwt.ExpiredSignature:
            msg = _('Signature has expired.')  #  The visa has expired 
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = _('Error decoding signature.')  #  Error decoding visa 
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise exceptions.AuthenticationFailed()  #  Invalid token 

        user = self.authenticate_credentials(payload)  #  If nothing goes wrong , Then execute here 

        return (user, jwt_value)  #  return user object , This will be assigned to reque.user, This meeting jwt The string is assigned to request.auth
        
=================Request.user About authority authentication 

        for authenticator in self.authenticators:
            try:
                user_auth_tuple = authenticator.authenticate(self)  #  There will be a return here 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  #  Assign a value  self.user=user ,self.auth = jwt_value
                return

   Let's see user How did it come out . It's in BaseJSONWebTokenAuthentication Class :

    def authenticate_credentials(self, payload):

        User = get_user_model() #  Back to the model !! It's not a record object  , adopt settings.py Medium AUTH_USER_MODEL To get 
        username = jwt_get_username_from_payload(payload)  #  obtain payload User name in 

        if not username:  #  If there is no user name, an exception is thrown 
            msg = _('Invalid payload.')
            raise exceptions.AuthenticationFailed(msg)

        try:  #  According to the user name in the load , In the model User Trying to retrieve the user's record object in , That is to say, database query will be used here .
            user = User.objects.get_by_natural_key(username)
        except User.DoesNotExist:  #  If User If the model does not exist, an exception will be thrown , explain 
            msg = _('Invalid signature.')
            raise exceptions.AuthenticationFailed(msg)
 
        if not user.is_active:  #  Determine whether the account is disabled 
            msg = _('User account is disabled.')
            raise exceptions.AuthenticationFailed(msg)

        return user  #  return user

no need auth Components

   After reading the source code analysis above , Let's think about it again , If not auth What to do with the module ?

   It's also very simple , We made our own jwt And then token Just return this random string .

   When authenticating, we can rewrite the authentication class , It's all that logic anyway .

  

Manual sign off

   We need to have... In our watch username as well as password Field , Use JWT Module token Several functions of , Make your own 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):  #  Sign at login 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: #  We can find out , Landing successful , Manual sign off 
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)
            return CommonResponse('100',' Landing successful ',data={'token':token})
        else:
            return CommonResponse('101', ' Login failed ')

   If you want to implement multi terminal login , mobile phone 、 user name 、 Email can log in , There's also a code here , It's just used with the serializer , More complicated :

#  Use the user name , cell-phone number , mailbox , You can log in #
#  The data format that the front end needs to transmit 
{
	"username":"lqz/1332323223/33@qq.com",  #  user name 、 Or cell phone number 、 Or email 
	"password":"lqz12345"
}


#  View 
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin, ViewSet
from app02 import ser
class Login2View(ViewSet):  #  Exactly the same as above 
    def login(self, request, *args, **kwargs):
        # 1  need   There's a serialized class 
        login_ser = ser.LoginModelSerializer(data=request.data,context={'request':request})  # context Parameters are similar to pipes , Connect to the serializer 
        # 2  Generate serialization class objects 
        # 3  Call the sequence number object is_validad
        login_ser.is_valid(raise_exception=True)
        token=login_ser.context.get('token')  #  Take out of the pipe token And back to 
        # 4 return
        return Response({'status':100,'msg':' Login successful ','token':token,'username':login_ser.context.get('username')})
    
    
    
#  Serialization class 
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()  #  Re cover username Field , In the data, it is unique,post, Think you save data , I have my own verification 
    class Meta:
        model=models.User
        fields=['username','password']

    def validate(self, attrs):

        print(self.context)

        #  Write logic here 
        username=attrs.get('username') #  There are three ways to use a user name 
        password=attrs.get('password')
        #  By judgment ,username The data are different , Query fields are different 
        #  Regular matching , If it's a cell phone number 
        if re.match('^1[3-9][0-9]{9}$',username):
            user=models.User.objects.filter(mobile=username).first()
        elif re.match('^.+@.+$',username):#  mailbox 
            user=models.User.objects.filter(email=username).first()
        else:
            user=models.User.objects.filter(username=username).first()
        if user: #  There are users 
            #  Check the password , Because it's ciphertext , Use check_password
            if user.check_password(password):
                #  Issue token
                payload = jwt_payload_handler(user)  #  hold user Pass in , obtain payload
                token = jwt_encode_handler(payload)  #  hold payload Pass in , obtain token
                self.context['token']=token
                self.context['username']=user.username
                return attrs
            else:
                raise ValidationError(' Wrong password ')
        else:
            raise ValidationError(' The user doesn't exist ')

JWT verification

   When verifying , Inherit BaseAuthentication class , Rewrite the validation method :

# 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  Fields are required ')
        try:
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignature:
            raise AuthenticationFailed(' Signature expired ')
        except jwt.InvalidTokenError:
            raise AuthenticationFailed(' Illegal users ')
        username = jwt_get_username_from_payload(payload)
        print(username)
        user = User.objects.filter(username=username).first()
        print(user)

        return user, jwt_value

   Then one of your views must be logged in before it can be accessed , Add it to the certification and it's just OK 了 .

from users.app_auth import JSONWebTokenAuthentication,MyJSONWebTokenAuthentication
class OrderView(APIView):
    # authentication_classes = [JSONWebTokenAuthentication]  #  There's no need to default , With our own 
    authentication_classes = [MyJSONWebTokenAuthentication]  #  Because it's not auth_user surface , So there's no need to judge whether it's an anonymous user .
    def get(self,request):
        print(request.user)
        return CommonResponse('100', ' success ',{' data ':' test '})

Finally, I want to say

  1. Pay attention to the prefix ,JWT start , A space , And then JWT Of token character string

  2. If you don't have to auth Components , It needs to be generated manually JWT Of token String and manual verification . It's troublesome , Fortunately JWT This module gives us a lot of convenience .

  3. Read more source code , Good for

版权声明
本文为[Mr. yunya]所创,转载请带上原文链接,感谢