新しいDjangoプロジェクトでのJWT認証の設定

この記事は、jangでのjwt認証のトピックに関する私の研究の結果として、いくつかの(最初の)記事をまとめたものです。そのため、(少なくともrunetでは)通常の記事を見つけることができませんでした。この記事は、プロジェクトの作成段階から、startproject、ねじ込みjwt認証を示しています。





徹底的に調査した上で、人間の判断に委ねます。





中古記事へのリンクが添付されています:





  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






JWT認証の構成

Djangoにはセッションベースの認証システムが付属しており、すぐに使用できます。これには、作成して後でユーザーにログインする必要がある可能性のあるすべてのモデル、ビュー、およびテンプレートが含まれます。ただし、ここに問題があります。Djangoのデフォルトの認証システムは、従来のHTML要求-応答ループでのみ機能します。





« '-' 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/), . , , .





この記事で行ったことを要約しましょう。柔軟なユーザーモデル(将来的には、必要に応じて拡張したり、さまざまなフィールドを追加したり、モデルを追加したりできます)、3つのシリアライザーを作成しました。各シリアライザーは、明確に定義された独自の機能を実行します。ユーザーが自分のアカウントに関する情報を登録、ログイン、受信、更新できるようにする4つのエンドポイントを作成しました。私の意見では、これは非常に快適な基盤であり、あなたの裁量でいくつかの新しいランププロジェクトを構築するための基礎です:)(ただし、何時間も話すことができる領域がたくさんあります。たとえば、次の段階はどのように言語で回転していますかpostgres、radishes、celeryをねじ込んで、すべてをドッカーコンテナに包みます。








All Articles