概要
カスタムユーザで
- 社員番号
- パスワード
を使ってログイン/ログアウトを行います
実際の挙動はSwaggerを使って確認します
必要な設定ファイルを記述
- models.py
- settings.py
- serilaizers.py
- views.py
- urls.py
に必要な情報を記載します
- models.py
- settings.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"]
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()
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を使ってください
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にします
@action(detail=False, methods=["POST"])
requestのdata内の情報は以下の通りです
print(request.data) # {'employee_number': '00000001', 'password': 'test'}
LoginSerializerにrequest.dataが入り、serilaizerの変数に代入されます
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のレスポンスが返ってきます
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_number
とpassword
の変数に代入していきます
Djangoにはauthenticate
というメソッドでユーザの認証を行うことができます
データベース内にuserが存在しない場合は400とエラーメッセージを返します
userが存在する場合はDjangoのlogin
メソッドが実行され、login
が成功します
今回はJsonResponseとしてroleが返ってくるよう設定します
authenticationメソッドについて詳しく知りたい方へ
今回は自身で作成した管理者ユーザで認証しているのでModelBackendクラスのauthenticationメソッドを実行しています
usernameとpasswordからユーザを特定し、ユーザが存在したらuserオブジェクトを返します
ユーザが存在しない場合はNoneを返します
また、今回はAbstractUserを継承したUserを作成しているため、AbstractUserで用意しているis_activeがFalseの場合もNoneを返します
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の時は別のエラーメッセージを出したいケースがあるかと思います
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オブジェクトを返します
class AllowAllUsersModelBackend(ModelBackend):
def user_can_authenticate(self, user):
return True
AllowAllUsersModelBackendを使用する際はsettings.pyに以下のように記載します
AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.AllowAllUsersModelBackend"]
logout
@action(methods=["POST"], detail=False)
def logout(self, request):
logout(request)
return HttpResponse()
logout
はlogin
と比べると簡単でDjangoのlogout
メソッドを使って実装します
まとめ
Djangoの
- is_valid
- authenticate
- login
- logout
メソッドを使うと楽に実装できました
しかし、現状の実装だけではログインしてもしなくてもAPIを使用できてしまっているので
下記のようにPermissionを実装するとログインしたユーザ以外はAPIを使うことができなくなります
興味がある方は見ていただけると幸いです
記事の紹介
以下の記事も書いたのでよかったら読んでみてください
参考