6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Django Rest Frameworkでログイン/ログアウト機能を実装してみよう!

Last updated at Posted at 2022-11-06

概要

カスタムユーザで

  • 社員番号
  • パスワード

を使ってログイン/ログアウトを行います
実際の挙動はSwaggerを使って確認します

必要な設定ファイルを記述

  • models.py
  • settings.py
  • serilaizers.py
  • views.py
  • urls.py

に必要な情報を記載します

  • models.py
  • settings.py

は下記の記事を参考に作成してください

serializers.py
from django.core.validators import RegexValidator
from rest_framework import serializers

from .models import User


class UserSerilaizer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id","employee_number","username", "email", "role"]
        read_only_fields = ["id", "created_at","updated_at"]


class LoginSerializer(serializers.ModelSerializer):
    employee_number = serializers.CharField(
        max_length=8,
        min_length=8,
        validators=[RegexValidator(r"^[0-9]{8}$")],
    )

    class Meta:
        model = User
        fields = ["employee_number","password"]
views.py
from django.contrib.auth import authenticate, login, logout
from django.http import HttpResponse, JsonResponse
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.viewsets import ModelViewSet
from rest_framework.viewsets import ViewSet

from .models import User
from .serializers import LoginSerializer, UserSerilaizer


class UserViewSet(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerilaizer


class LoginViewSet(ViewSet):
    serializer_class = LoginSerializer
    permission_classes = [AllowAny]

    @action(detail=False, methods=["POST"])
    def login(self, request):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        user = authenticate(
            request=request,
            username=serializer.validated_data["employee_number"],
            password=serializer.validated_data["password"],
        )
        if not user:
            return JsonResponse(
                data={"msg": "社員番号またはパスワードが間違っています"},
                status=status.HTTP_400_BAD_REQUEST,
            )
        else:
            login(request, user)
            return JsonResponse(data={"role": user.Role(user.role).name})

    @action(methods=["POST"], detail=False)
    def logout(self, request):
        logout(request)
        return HttpResponse()
アプリケーション/urls.py
from django.urls import path, include
from rest_framework_nested import routers

from application.views import (
    UserViewSet,
)

router = routers.DefaultRouter()
router.register(r'users', UserViewSet, basename='user')

urlpatterns = [
    path(r'', include(router.urls)),
]

プロジェクトのurlとSwaggerの設定をしたい場合は下記の記事を参考にしてください

では、一つずつ解説していきます

serilaizers.py

今回は

  • 社員番号
  • パスワード

でログインするため、LoginSerilaizerを新規で作成し、

  • employee_number
  • password

のみを対象にします

どうしてemployee_numberをオーバーライドするの?

employee_numberはuniqueな値です
loginする際はPOSTリクエストを送るのでデータベースにすでにそのユーザが存在するエラーが発生するのを防ぐために行います
仮にオーバーライドしないと以下のようなエラーが表示されます

{
  "employee_number": [
    "この 社員番号 を持った user が既に存在します。"
  ]
}

views.py

login

今回は

  • 社員番号:00000001
  • パスワード:test

のユーザでログインを行います

ご自身でもユーザを入れて検証してみたい方はcreatesuperuserでデータを入れるかfixtureを使ってください

views.py
from django.contrib.auth import authenticate, login, logout
from django.http import HttpResponse, JsonResponse
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.viewsets import ModelViewSet
from rest_framework.viewsets import ViewSet

from .models import User
from .serializers import LoginSerializer, UserSerilaizer


class UserViewSet(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerilaizer


class LoginViewSet(ViewSet):
    serializer_class = LoginSerializer
    permission_classes = [AllowAny]

    @action(detail=False, methods=["POST"])
    def login(self, request):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        user = authenticate(
            request=request,
            username=serializer.validated_data["employee_number"],
            password=serializer.validated_data["password"],
        )
        if not user:
            return JsonResponse(
                data={"msg": "社員番号またはパスワードが間違っています"},
                status=status.HTTP_400_BAD_REQUEST,
            )
        else:
            login(request, user)
            return JsonResponse(data={"role": user.Role(user.role).name})

今回はPOSTのみを実装したいので@actionデコレータを使用します
ログインする際はrequest内の情報だけで十分なのでdetail=Falseにします

views.py
@action(detail=False, methods=["POST"])

requestのdata内の情報は以下の通りです

print(request.data) # {'employee_number': '00000001', 'password': 'test'}

LoginSerializerにrequest.dataが入り、serilaizerの変数に代入されます

views.py
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
serializer.is_valid()

でバリデーションを行います。これを行わないと以下のエラーが表示されます

When a serializer is passed a `data` keyword argument you must call `.is_valid()` before attempting to access the serialized `.data` representation.
You should either call `.is_valid()` first, or access `.initial_data` instead.

今回はModelで社員番号は8桁でバリデーションをかけているので例えば
8桁を超える社員番号を入れたとするとバリデーションエラーが発生し、400のレスポンスが返ってきます

スクリーンショット 2022-11-06 14.28.47.png

スクリーンショット 2023-12-30 8.55.33.png

views.py
        user = authenticate(
            request=request,
            username=serializer.validated_data["employee_number"],
            password=serializer.validated_data["password"],
        )
        if not user:
            return JsonResponse(data={"msg": "社員番号またはパスワードが間違っています"}, status=status.HTTP_400_BAD_REQUEST)
        else:
            login(request, user)
            return JsonResponse(data={"role": user.Role(user.role).name})

serializer.dataの中身は以下の通りになっています

print(serializer.data) # {'employee_number': '00000001', 'password': 'test'}

employee_numberpasswordの変数に代入していきます
Djangoにはauthenticateというメソッドでユーザの認証を行うことができます
データベース内にuserが存在しない場合は400とエラーメッセージを返します
スクリーンショット 2022-11-06 14.48.35.png

userが存在する場合はDjangoのloginメソッドが実行され、loginが成功します

スクリーンショット 2022-11-06 14.56.45.png

今回はJsonResponseとしてroleが返ってくるよう設定します

authenticationメソッドについて詳しく知りたい方へ

今回は自身で作成した管理者ユーザで認証しているのでModelBackendクラスのauthenticationメソッドを実行しています
usernameとpasswordからユーザを特定し、ユーザが存在したらuserオブジェクトを返します
ユーザが存在しない場合はNoneを返します
また、今回はAbstractUserを継承したUserを作成しているため、AbstractUserで用意しているis_activeがFalseの場合もNoneを返します

django.contrib.auth.backends.py
class BaseBackend:
    def authenticate(self, request, **kwargs):
        return None


class ModelBackend(BaseBackend):
    """
    Authenticates against settings.AUTH_USER_MODEL.
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        if username is None or password is None:
            return
        try:
            user = UserModel._default_manager.get_by_natural_key(username)
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a nonexistent user (#20760).
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user

    def user_can_authenticate(self, user):
        """
        Reject users with is_active=False. Custom user models that don't have
        that attribute are allowed.
        """
        return getattr(user, "is_active", True)

is_active=Falseの時もログイン処理を行いたい場合は?

例えば下記みたいにis_active=Falseの時は別のエラーメッセージを出したいケースがあるかと思います

views.py
class LoginViewSet(ViewSet):
    serializer_class = LoginSerializer
    permission_classes = [AllowAny]

    @action(detail=False, methods=["POST"])
    def login(self, request):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        # authenticateはUserモデルのis_activeがFalseの場合、Noneを返却する
        user = authenticate(
            request=request,
            username=serializer.validated_data["employee_number"],
            password=serializer.validated_data["password"],
        )

        if user is None:
            return JsonResponse(
                data={"msg": "社員番号、またはパスワードが間違っています。"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        if not user.is_active:
            return JsonResponse(
                data={"msg": "管理者に問い合わせてください。"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        login(request, user)
        return JsonResponse(data={"role": user.Role(user.role).name})

その場合はDjangoのAllowAllUsersModelBackendクラスを使用します
このクラスを使用することでuser_can_authenticateメソッドが実行する際にis_active=Falseでもuserオブジェクトを返します

django.contrib.auth.backends.py
class AllowAllUsersModelBackend(ModelBackend):
    def user_can_authenticate(self, user):
        return True

AllowAllUsersModelBackendを使用する際はsettings.pyに以下のように記載します

settings.py
AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.AllowAllUsersModelBackend"]

logout

views.py
    @action(methods=["POST"], detail=False)
    def logout(self, request):
        logout(request)
        return HttpResponse()

logoutloginと比べると簡単でDjangoのlogoutメソッドを使って実装します
スクリーンショット 2022-11-06 14.59.07.png

まとめ

Djangoの

  • is_valid
  • authenticate
  • login
  • logout

メソッドを使うと楽に実装できました
しかし、現状の実装だけではログインしてもしなくてもAPIを使用できてしまっているので
下記のようにPermissionを実装するとログインしたユーザ以外はAPIを使うことができなくなります
興味がある方は見ていただけると幸いです

記事の紹介

以下の記事も書いたのでよかったら読んでみてください

参考

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?