Einrichten der JWT-Authentifizierung in einem neuen Django-Projekt

Dieser Artikel ist eine Zusammenstellung mehrerer (basierend auf dem ersten) Artikel als Ergebnis meiner Studien zum Thema JWT-Authentifizierung in Jang mit allem, was dies impliziert. Es war also nicht möglich (zumindest in Runet), einen normalen Artikel zu finden, der von der Phase der Erstellung eines Projekts, des Startprojekts und der JWT-Authentifizierung erzählt.





Nach gründlichen Nachforschungen unterziehe ich mich dem menschlichen Urteil.





Links zu gebrauchten Artikeln sind beigefügt:





  1. https://thinkster.io/tutorials/django-json-api/authentication





  2. https://simpleisbetterthancomplex.com/tutorial/2018/12/19/how-to-use-jwt-authentication-with-django-rest-framework.html





  3. https://www.django-rest-framework.org/api-guide/authentication/





  4. https://medium.com/django-rest/django-rest-framework-jwt-authentication-94bee36f2af8






Konfigurieren der JWT-Authentifizierung

Django wird mit einem sitzungsbasierten Authentifizierungssystem geliefert und funktioniert sofort. Dies umfasst alle Modelle, Ansichten und Vorlagen, die Sie möglicherweise zum Erstellen und späteren Anmelden von Benutzern benötigen. Aber hier ist der Haken: Das Standardauthentifizierungssystem von Django funktioniert nur mit der traditionellen HTML-Anforderungs-Antwort-Schleife.





« '-' HTML»? , - (, ), . , «», - , - , , HTML , . , , « ».





, Django '-' HTML? , API, . , , JSON, HTML. JSON, , , . '-' JSON, , ( '-' HTML), . .





, Django , . , , . , . , , Django, , , .





, , :





  1. User, Django





  2. JSON HTML





  3. HTML, Django





,

, Django . , , , , JSON Web Token Authentication (JWT ), .





Django (cookie). , (middlewares) , , . request.user



. , request.user



User



. , request.user



AnonymousUser



. , , request.user .





? , , , , request.user.isauthenticated()



, True



False



. request.user



AnonymousUser



, request.user.isauthenticated()



False. ( :) )





if request.user is not None and request.user.isauthenticated():



if request.user.isauthenticated():







, - !





, . , http://localhost:3000, http://localhost:5000. , , http://www.server.com http://www.clent.com. cookie, , , .





, cookie, (Cross-Origin Resource Sharing, CORS) (Cross-Site Request Forgery, CSRF), :





CORS





CSRF





,

/ .. . . . ( , , , , , , ""). , , . , . , , . , , . , , .





(ID) . , . , . - , . , .





JSON Web Tokens

JSON Web Token (. JWT) - (RFC 7519) , . JWT .





, , ? JWT , .





JSON Web Tokes ?

JWT :





  1. JWT - . , JWT , . , , .





  2. JWT , .





  3. . , « » , .





,

, . , django-admin startproject json_auth_project



( , pip3 install django



).





. cd jsonauthproject



, python3 -m venv venv



. , venv



, . , . ./venv/bin/activate



. requirements.txt



, ( django



pip3 install django



). pip3 freeze > requirements.txt



( , / ). ./manage.py migrate



. , , . - ./manage.py runserver



. localhost



8000. http://localhost:8000. "The install worked successfully! Congratulations!" - :)





, (app) authentication



: ./manage.py startapp authentication



. apps/authentication/models.py



, . , .





User



UserManager



, :





import jwt

from datetime import datetime, timedelta

from django.conf import settings from django.contrib.auth.models import (
	AbstractBaseUser, BaseUserManager, PermissionsMixin
)

from django.db import models
      
      



Django Manager : createuser()



createsuperuser()



. Django, https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#substituting-a-custom-user-model





UserManager



apps/authentication/models.py



( : https://docs.djangoproject.com/en/3.1/topics/db/managers/):





class UserManager(BaseUserManager):
    """
    Django ,      
     Manager.   BaseUserManager,    
      ,  Django    User ( ).
    """

    def create_user(self, username, email, password=None):
        """      ,   . """
        if username is None:
            raise TypeError('Users must have a username.')

        if email is None:
            raise TypeError('Users must have an email address.')

        user = self.model(username=username, email=self.normalize_email(email))
        user.set_password(password)
        user.save()

        return user

    def create_superuser(self, username, email, password):
        """       . """
        if password is None:
            raise TypeError('Superusers must have a password.')

        user = self.create_user(username, email, password)
        user.is_superuser = True
        user.is_staff = True
        user.save()

        return user
      
      



, , , :





class User(AbstractBaseUser, PermissionsMixin):
    #       ,
    #       User  
    # .          
    #     .
    username = models.CharField(db_index=True, max_length=255, unique=True)

    #      ,      
    #          .
    #        ,   
    #      ,    
    #        (  ).
    email = models.EmailField(db_index=True, unique=True)

    #        ,  
    #    .    ,   
    #    ,       :)   
    #        .
    #  ,      ,     
    #   .
    is_active = models.BooleanField(default=True)

    #   ,       
    # .       .
    is_staff = models.BooleanField(default=False)

    #    .
    created_at = models.DateTimeField(auto_now_add=True)

    #       .
    updated_at = models.DateTimeField(auto_now=True)

    #  ,  Django
    #     .

    #  USERNAME_FIELD  ,     
    #    .       .
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    #  Django,     UserManager
    #     .
    objects = UserManager()

    def __str__(self):
        """    (  ) """
        return self.email

    @property
    def token(self):
        """
              user.token, 
        user._generate_jwt_token().  @property   
        . token  " ".
        """
        return self._generate_jwt_token()

    def get_full_name(self):
        """
           Django   ,   
        .     ,    
         ,   username.
        """
        return self.username

    def get_short_name(self):
        """   get_full_name(). """
        return self.username

    def _generate_jwt_token(self):
        """
         - JSON,     
        ,     1   
        """
        dt = datetime.now() + timedelta(days=1)

        token = jwt.encode({
            'id': self.pk,
            'exp': int(dt.strftime('%s'))
        }, settings.SECRET_KEY, algorithm='HS256')

        return token.decode('utf-8')
      
      



, :





  1. models.CustomUser



    - , Django User



    https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#django.contrib.auth.models.CustomUser





  2. models.AbstractBaseUser



    models.PermissionsMixin



    - https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#django.contrib.auth.models.AbstractBaseUser https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#django.contrib.auth.models.PermissionsMixin





  3. models.BaseUserManager



    - UserManager



    https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#django.contrib.auth.models.BaseUserManager





  4. , Django, , (, db_index



    unique



    ) https://docs.djangoproject.com/en/3.1/ref/models/fields/





AUTH_USER_MODEL

-, Django , - django.contrib.auth.models.User



. , . User



, , Django User



, .





Django: https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#substituting-a-custom-user-model





, User



, .





Django User



, AUTH_USER_MODEL



project/settings.py



. , project/settings.py



:





#  Django      . 
# authentication.User  Django,      User  
# authentication.       INSTALLED_APPS.
AUTH_USER_MODEL = 'authentication.User'
      
      



, , . - , Django , , - . , User.





  • : ./manage.py makemigrations



    ./manage.py migrate



    , , . SQLite , . Django , AUTH_USER_MODEL



    , .





. . , :





./manage.py makemigrations







Django. , . , , .





authenticate



,





./manage.py makemigrations authentication







authentication



. . ,





./manage.py makemigrations







:





./manage.py migrate







makemigrations



, migrate



.





, User, , . , User. , .





:





./manage.py createsuperuser







Django - , . , . ! :)





, Django, :





./manage.py shell_plus



( ./manage.py shell



)





shell_plus django-extensions, (pip3 install django-extensions), shell_plus. , , INSTALLED_APPS. , .





, :





user = User.objects.first()
user.username
user.token
      
      



, username



token



.





, . .





RegistrationSerializer





apps/authentication/serializers.py



:





from rest_framework import serializers

from .models import User


class RegistrationSerializer(serializers.ModelSerializer):
    """      . """

    # ,      8 ,   128,
    #           
    password = serializers.CharField(
        max_length=128,
        min_length=8,
        write_only=True
    )

    #          
    #   .      .
    token = serializers.CharField(max_length=255, read_only=True)

    class Meta:
        model = User
        #   ,      
        #  ,  ,   .
        fields = ['email', 'username', 'password', 'token']

    def create(self, validated_data):
        #   create_user,  
        #  ,    .
        return User.objects.create_user(**validated_data)
      
      



, , .





ModelSerializer

RegistrationSerializer



, serializers.ModelSerializer



. serializers.ModelSerializer



- serializers.Serializer,



Django REST Framework (DFR). ModelSerializer



, Django. , : . create()



User.objects.create_user()



, . DRF .





RegistrationAPIView





. , (views) (endpoint), URL .





apps/authentication/views.py



:





from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView

from .serializers import RegistrationSerializer


class RegistrationAPIView(APIView):
    """
       (  )    .
    """
    permission_classes = (AllowAny,)
    serializer_class = RegistrationSerializer

    def post(self, request):
        user = request.data.get('user', {})

        #   ,    - 
        # ,        .
        serializer = self.serializer_class(data=user)
        serializer.is_valid(raise_exception=True)
        serializer.save()

        return Response(serializer.data, status=status.HTTP_201_CREATED)
      
      



:





  1. permission_classes



    - , , . .. ..





  2. , , post - , . .





Django REST Framework (DRF) Permissions https://www.django-rest-framework.org/api-guide/permissions/





. Django 1.x => 2.x URL (path). URL- URL's, . , Django , .





apps/authentication/urls.py



:





from django.urls import path

from .views import RegistrationAPIView

app_name = 'authentication'
urlpatterns = [
    path('users/', RegistrationAPIView.as_view()),
]
      
      



Django . . . app_name = 'authentication'



, (including) . URL-.





project/urls.py



:





from django.urls import path







, , include()



django.urls







from django.urls import path, include







include()



, , .





:





urlpatterns = [
    path('admin/', admin.site.urls),
]
      
      



, urls.py



:





urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('apps.authentication.urls', namespace='authentication')),
]
      
      



Postman

, User , , , . ( ) Postman ( https://learning.postman.com/docs/getting-started/introduction/).





POST localhost:8000/api/users/ :





{
    "user": {
        "username": "user1",
        "email": "user1@user.user",
        "password": "qweasdzxc"
    }
}

      
      



. ! , , . , "user". ( , ), DRF (renderer).





User

apps/authentication/renderers.py



:





import json

from rest_framework.renderers import JSONRenderer


class UserJSONRenderer(JSONRenderer):
    charset = 'utf-8'

    def render(self, data, media_type=None, renderer_context=None):
        #     token   ,   
        # .    ,   
        #      User.
        token = data.get('token', None)

        if token is not None and isinstance(token, bytes):
            #   ,  token     bytes.
            data['token'] = token.decode('utf-8')

        # ,         'user'.
        return json.dumps({
            'user': data
        })
      
      



, .





, apps/auhentication/views.py



UserJSONRenderer



, :





from .renderers import UserJSONRenderer







, renderer_classes



RegistrationAPIView



:





renderer_classes = (UserJSONRenderer,)
      
      



, UserJSONRenderer



, Postman'e . , "user".





, . , , . , API .





LoginSerializer





apps/authentication/serializers.py



:





from django.contrib.auth import authenticate
      
      



, :





class LoginSerializer(serializers.Serializer):
    email = serializers.CharField(max_length=255)
    username = serializers.CharField(max_length=255, read_only=True)
    password = serializers.CharField(max_length=128, write_only=True)
    token = serializers.CharField(max_length=255, read_only=True)

    def validate(self, data):
        #   validate  ,   
        # LoginSerializer  valid.      
        #    ,    
        #   ,       .
        email = data.get('email', None)
        password = data.get('password', None)

        #  ,    .
        if email is None:
            raise serializers.ValidationError(
                'An email address is required to log in.'
            )

        #  ,    .
        if password is None:
            raise serializers.ValidationError(
                'A password is required to log in.'
            )

        #  authenticate  Django   , 
        #      -  
        #   .   email  username,    
        #  USERNAME_FIELD = email.
        user = authenticate(username=email, password=password)

        #     /  ,  authenticate
        #  None.     .
        if user is None:
            raise serializers.ValidationError(
                'A user with this email and password was not found.'
            )

        # Django   is_active   User.  
        # ,      .
        #  ,     True.
        if not user.is_active:
            raise serializers.ValidationError(
                'This user has been deactivated.'
            )

        #  validate     . 
        # ,    ..   create  update.
        return {
            'email': user.email,
            'username': user.username,
            'token': user.token
        }
      
      



, .





LoginAPIView





apps/authentication/views.py



:





from .serializers import LoginSerializer, RegistrationSerializer
      
      



, :





class LoginAPIView(APIView):
    permission_classes = (AllowAny,)
    renderer_classes = (UserJSONRenderer,)
    serializer_class = LoginSerializer

    def post(self, request):
        user = request.data.get('user', {})

        #  ,      save() , 
        #    .   ,     
        #  .  ,  validate()   .
        serializer = self.serializer_class(data=user)
        serializer.is_valid(raise_exception=True)

        return Response(serializer.data, status=status.HTTP_200_OK)
      
      



, apps/authentication/urls.py



:





from .views import LoginAPIView, RegistrationAPIView
      
      



urlpatterns



:





urlpatterns = [
    path('users/', RegistrationAPIView.as_view()),
    path('users/login/', LoginAPIView.as_view()),
]
      
      



Postman

, , . :) Postman, http://localhost:8000/api/users/login/



, . , :





{
    "user": {
        "email": "email@email.email",
        "username": "admin",
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjA1MTE3MjkwfQ.W8B6RY-jGO9PYDTzDWxhrkSHsTe1p3jlzq1BL7Tbwcs"
    }
}
      
      



, token



, , .





-, . , , . - . -, non_field_errors



. , . , , , validate_email



, Django REST Framework , . , nonfield_errors



, , . -, , JSON ( ). , Django REST Framework.





EXCEPTION_HANDLER NON_FIELD_ERRORS_KEY

DRF EXCEPTION_HANDLER



. , , EXCEPTION_HANDLER



. NON_FIELD_ERRORS_KEY



, .





project/exceptions.py



, :





from rest_framework.views import exception_handler


def core_exception_handler(exc, context):
    #   ,      , 
    #      -, 
    # DRF.   ,      ,  
    #    DRF -    .
    response = exception_handler(exc, context)
    handlers = {
        'ValidationError': _handle_generic_error
    }
    #    .     ,
    #  ,         DRF.
    exception_class = exc.__class__.__name__

    if exception_class in handlers:
        #      -  :)  
        # ,      
        return handlers[exception_class](exc, context, response)

    return response


def _handle_generic_error(exc, context, response):
    #     ,    . 
    #    DRF      'errors'.
    response.data = {
        'errors': response.data
    }

    return response
      
      



, project/settings.py



REST_FRAMEWORK



:





REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'project.exceptions.core_exception_handler',
    'NON_FIELD_ERRORS_KEY': 'error',
}
      
      



DFR. , , , . ( / ) Postman - .





UserJSONRenderer

, / , . , "error", "user", . UserJSONRenderer



"error" . apps/authenticate/renderers.py



:





import json

from rest_framework.renderers import JSONRenderer


class UserJSONRenderer(JSONRenderer):
    charset = 'utf-8'

    def render(self, data, media_type=None, renderer_context=None):
        #     (,   
        #  ), data    error.  ,
        #   JSONRenderer   , 
        #    .
        errors = data.get('errors', None)

        #     token   ,   
        # .    ,   
        #      User.
        token = data.get('token', None)

        if errors is not None:
            #   JSONRenderer  .
            return super(UserJSONRenderer, self).render(data)

        if token is not None and isinstance(token, bytes):
            #   ,  token     bytes.
            data['token'] = token.decode('utf-8')

        # ,         'user'.
        return json.dumps({
            'user': data
        })
      
      



, ( /) Postman - .





.

, . . .





UserSerializer





. , .





apps/authentication/serializers.py



:





class UserSerializer(serializers.ModelSerializer):
    """      User. """

    #     8  128 .   . 
    #     -,      
    # ,    ,     .
    password = serializers.CharField(
        max_length=128,
        min_length=8,
        write_only=True
    )

    class Meta:
        model = User
        fields = ('email', 'username', 'password', 'token',)

        #  read_only_fields     
        #   read_only = True,       .
        # ,       'read_only_fields'
        #   ,        .  
        #    min_length  max_length,
        #       .
        read_only_fields = ('token',)

    def update(self, instance, validated_data):
        """   User. """

        #     ,      
        # setattr. Django  ,   
        #   ''.  ,     
        #    'validated_data'    .
        password = validated_data.pop('password', None)

        for key, value in validated_data.items():
            #  ,   validated_data   
            #    User  .
            setattr(instance, key, value)

        if password is not None:
            # 'set_password()'   ,   
            #   ,       .
            instance.set_password(password)

        #  ,    ,     
        # User.  ,  set_password()   .
        instance.save()

        return instance
      
      



, create , DRF serializers.ModelSerializer



. , , RegistrationSerializer



.





UserRetrieveUpdateAPIView





apps/authentication/views.py



:





from rest_framework import status
from rest_framework.generics import RetrieveUpdateAPIView
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from .renderers import UserJSONRenderer
from .serializers import (
    LoginSerializer, RegistrationSerializer, UserSerializer,
)
      
      



, UserRetrieveUpdateView



:





class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
    permission_classes = (IsAuthenticated,)
    renderer_classes = (UserJSONRenderer,)
    serializer_class = UserSerializer

    def retrieve(self, request, *args, **kwargs):
        #     .   , 
        #     User  -, 
        #    json   .
        serializer = self.serializer_class(request.user)

        return Response(serializer.data, status=status.HTTP_200_OK)

    def update(self, request, *args, **kwargs):
        serializer_data = request.data.get('user', {})

        #  ,    - ,   
        serializer = self.serializer_class(
            request.user, data=serializer_data, partial=True
        )
        serializer.is_valid(raise_exception=True)
        serializer.save()

        return Response(serializer.data, status=status.HTTP_200_OK)
      
      



apps/authentication/urls.py



, UserRetrieveUpdateView



:





from .views import (
    LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView
)
      
      



urlpatterns



:





urlpatterns = [
    path('user', UserRetrieveUpdateAPIView.as_view()),
    path('users/', RegistrationAPIView.as_view()),
    path('users/login/', LoginAPIView.as_view()),
]
      
      



Postman (GET localhost:8000/api/user/). , :





{
    "user": {
        "detail": "Authentication credentials were not provided."
    }
}
      
      



Django . , - , , , . JWT, Django, Django REST Framework (DRF).





apps/authentication/backends.py



:





import jwt

from django.conf import settings

from rest_framework import authentication, exceptions

from .models import User


class JWTAuthentication(authentication.BaseAuthentication):
    authentication_header_prefix = 'Token'

    def authenticate(self, request):
        """
         authenticate   ,   , 
           . 'authenticate'   
         :
            1) None -   None    .
              ,   ,    .
              , , ,     
            .
            2) (user, token) -    /
            ,    .    
              ,  ,   ,  
              .       
            AuthenticationFailed   DRF   .
        """
        request.user = None

        # 'auth_header'      :
        # 1)    (Token   )
        # 2)  JWT,      
        auth_header = authentication.get_authorization_header(request).split()
        auth_header_prefix = self.authentication_header_prefix.lower()

        if not auth_header:
            return None

        if len(auth_header) == 1:
            #   ,     
            return None

        elif len(auth_header) > 2:
            #   , -   
            return None

        # JWT    ,   
        #  bytes,     
        # Python3 (HINT:  PyJWT).    ,  
        #  prefix  token.     ,   
        # ,    ,    .
        prefix = auth_header[0].decode('utf-8')
        token = auth_header[1].decode('utf-8')

        if prefix.lower() != auth_header_prefix:
            #    ,    - .
            return None

        #     "",    .
        #        .
        return self._authenticate_credentials(request, token)

    def _authenticate_credentials(self, request, token):
        """
            .   -
           ,  -  .
        """
        try:
            payload = jwt.decode(token, settings.SECRET_KEY)
        except Exception:
            msg = ' .   '
            raise exceptions.AuthenticationFailed(msg)

        try:
            user = User.objects.get(pk=payload['id'])
        except User.DoesNotExist:
            msg = '     .'
            raise exceptions.AuthenticationFailed(msg)

        if not user.is_active:
            msg = '  .'
            raise exceptions.AuthenticationFailed(msg)

        return (user, token)
      
      



, . , , , , , , - .





DRF

Django REST Framework, , , Django .





project/settings.py



REST_FRAMEWORK



:





REST_FRAMEWORK = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'apps.authentication.backends.JWTAuthentication',
    ),
}
      
      



Postman

, , , , . , Postman (GET localhost:8000/api/user/). , . , ? . (PATCH localhost:8000/api/user/), . , , .





Lassen Sie uns zusammenfassen, was wir in diesem Artikel getan haben. Wir haben ein flexibles Benutzermodell erstellt (in Zukunft können Sie es beliebig erweitern, indem Sie verschiedene Felder, zusätzliche Modelle usw. hinzufügen), drei Serialisierer, von denen jeder seine eigene klar definierte Funktion ausführt. Es wurden vier Endpunkte erstellt, mit denen Benutzer sich registrieren, anmelden, ihre Kontoinformationen empfangen und aktualisieren können. Meiner Meinung nach ist dies eine sehr angenehme Grundlage, auf der Sie nach eigenem Ermessen ein neues Lampenprojekt erstellen können :) (es gibt jedoch eine Reihe von Bereichen, über die Sie stundenlang sprechen können, zum Beispiel dreht sich die nächste Stufe in der Sprache darüber, wie wickeln Sie alles in Docker-Behälter ein, indem Sie Postgres, Radieschen und Sellerie einschrauben.








All Articles