Djangoの認証機能はデフォルトでユーザー名とパスワードでのログインができます。
しかし、最近はユーザー名かメールアドレス、どちらでも使える場合が多いです。
カスタマイズ方法には
- フォームをカスタマイズする
- バックエンド認証機能をカスタマイズする
の2パターンがあります。
認証機能を拡張して、ログインを使いやすくしていきましょう。
ユーザー名でもメールアドレスでもログインできるようにする
準備:ユーザーモデルをカスタマイズする
Djangoはデフォルトで用意されているUserモデルを使用することを非推奨としています。
それゆえに今回は新しくCustomUserというクラスを作成することにします。
今回はログインが主ですので詳細な説明はしません。
また既にユーザーモデルをカスタマイズしている方は読み飛ばしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
from django.contrib.auth.models import BaseUserManager from django.contrib.auth.models import AbstractUser from django.db import models #新規作成を担当する新規マネージャークラス class CustomUserManager(BaseUserManager): def create_user(self, email, password, **extra_fields): if not email: raise ValueError('The Email field must be set') email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, email, password=None, **extra_fields): extra_fields.setdefault('is_staff', True) extra_fields.setdefault('is_superuser', True) return self.create_user(email, password, **extra_fields) #当グルメサイト専用のユーザーモデル class CustomUser(AbstractUser): username = models.CharField(max_length=255, unique=True) email = models.EmailField(max_length=255, unique=True) USERNAME_FIELD = 'username' EMAIL_FIELD = 'email' REQUIRED_FIELDS = ['email'] objects = CustomUserManager() def __str__(self): return self.email |
CustomUserクラスを用いてログイン機能をカスタマイズします。
①フォームをカスタマイズする方法
この方法ではforms.pyとviews.pyをカスタマイズします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
from django import forms from django.contrib.auth import authenticate from .models import CustomUser class CustomAuthenticationForm(forms.Form): username_or_email = forms.CharField() password = forms.CharField(widget=forms.PasswordInput) def clean(self): username_or_email = self.cleaned_data.get('username_or_email') password = self.cleaned_data.get('password') if username_or_email and password: if '@' in username_or_email: kwargs = {'email': username_or_email} else: kwargs = {'username': username_or_email} try: user = CustomUser.objects.get(**kwargs) if not user.check_password(password): raise forms.ValidationError('Incorrect password.') except CustomUser.DoesNotExist: raise forms.ValidationError('User does not exist.') user = authenticate(username=user.username, password=password) if not user: raise forms.ValidationError('Failed to authenticate user.') return super(CustomAuthenticationForm, self).clean() |
このクラスはログインフォームを表示するためのクラスとなっていて、cleanメソッドを拡張しています。
cleanメソッドはフォームの値のバリデーションを行うメソッドとなっていて、その一番下で認証を行っています。
authenticate関数が認証を行う関数です。
ログイン処理というのは3段階に分かれ、値のバリデーション→認証→ログインという形になります。
このフォームクラスでは、値のバリデーションと認証を行っていることになります。
「@」が入っていればメールアドレス。そうでなければユーザー名として処理を分岐しています。
今回は簡易な方法でメールアドレスかどうかを判断したために、ユーザー登録の段階でユーザー名に「@」を使えなくする必要があります。
もっと厳密にメールアドレスだと判断したい場合は、正規表現などを使用すると良いでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
from django.views.generic.edit import FormView from .forms import CustomAuthenticationForm from django.urls import reverse_lazy from .models import CustomUser from django.contrib.auth import login class UserLoginView(FormView): template_name = 'certification/login.html' form_class = CustomAuthenticationForm success_url = reverse_lazy('top') def form_valid(self, form): username_or_email = form.cleaned_data.get('username_or_email') password = form.cleaned_data.get('password') # Similar to what we did in the form, we determine if the user input is an email or username. if '@' in username_or_email: kwargs = {'email': username_or_email} else: kwargs = {'username': username_or_email} user = CustomUser.objects.get(**kwargs) login(self.request,user) return super().form_valid(form) |
Viewではログインの3段階の内、最後であるログイン処理を行っています。
②バックエンド認証機能をカスタマイズする方法
続いてはDjangoのバックエンド側にある認証システムをそもそもカスタマイズしてしまう方法です。
こちらの手法の方が多く見受けられる気がします。
まず、アプリのフォルダ内にauth_backends.pyというファイルを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend class UsernameOrEmailBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): UserModel = get_user_model() try: # ユーザー名またはメールアドレスでユーザーを取得 if '@' in username: user = UserModel.objects.get(email=username) else: user = UserModel.objects.get(username=username) except UserModel.DoesNotExist: return None # パスワードが正しいかチェック if user.check_password(password): return user def get_user(self, user_id): UserModel = get_user_model() try: return UserModel.objects.get(pk=user_id) except UserModel.DoesNotExist: return None |
ModelBackendクラスを拡張しています。
内容に関しては①で行ったようなユーザー名かメールアドレスかの判断、バリデーションです。
ログインをさらに強固にするならば、もっとバリデーションの種類を増やしても良いでしょう。
このファイルを認識させるために、プロジェクトのsettings.pyに追記します。
1 2 3 |
AUTHENTICATION_BACKENDS = [ '[アプリ名].auth_backends.UsernameOrEmailBackend', ] |
こちらの方法でもforms.pyに追記が必要ですが、その記述量は①と比べてかなり削減されています。
1 2 3 4 |
from django.contrib.auth.forms import AuthenticationForm class CustomAuthenticationForm2(AuthenticationForm): username = forms.CharField(label='Username or Email', max_length=254) |
認証に関しては先ほど作成したauth_backends.pyが担ってくれているので、フォームの表示を変えているだけです。
同様にViewに関しても記述量が減っています。
1 2 3 4 5 6 |
from django.contrib.auth import views as auth_views from .forms import CustomAuthenticationForm2 class UserLoginView2(auth_views.LoginView): form_class = CustomAuthenticationForm2 template_name = 'certification/login.html' |
先ほど設定したformクラスとテンプレートを指定するだけのシンプルな実装となっています。
おわりに
2つの方法を紹介しましたが、認証バックエンド機能に関してはさまざまな場所で認証する場合に便利な手法です。
一方で、小規模のシステムでログインくらいにしか認証を使用しない場合にはフォームのカスタマイズで事足りるでしょう。