今回のテーマは「3-11. 認証バックエンドをカスタマイズしてログイン方法を変更する」です。これまで、Django標準のログイン方法としてユーザー名とパスワードでログインするログイン画面でした。しかしメールアドレスでログインする方法に変更したい場合もあると思います。今回はDjangoのユーザー認証について見ていきましょう。
※本ページは「3-10. 独自カスタマイズのユーザーを使用する」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。
Djangoのログインの仕組み
djangoの認証は認証用のバックエンドによって行われます。このバックエンドはsettings.pyのAUTHENTICATION_BACKENDSに設定します。AUTHENTICATION_BACKENDSのデフォルト値は’django.contrib.auth.backends.ModelBackend’1つですが、バックエンドは配列として追加することが出来ます。
Djangoの認証はdjango.contrib.auth.authenticate関数で行われます。このauthenticate関数が呼ばれるとAUTHENTICATION_BACKENDSに記載されたバックエンドのauthenticate関数を順番に呼び出します。認証に失敗すれば次のバックエンドを呼び出します。もし認証に成功すれば、認証されたユーザーを返す仕組みです。
今回は独自に実装したフォームの中のclean関数をオーバーライドしてauthenticate関数を呼び出し認証処理を行います。もし認証に失敗した場合はバリデーションエラーを返す仕組みです。
認証バックエンドをカスタマイズする
デフォルトのModelBackendに加えてカスタマイズしたバックエンドを追加しましょう。accounts/backends.pyを新規作成します。ファイル名はこの名前でなくても大丈夫です。
accounts/backends.py
from django.contrib.auth.backends import ModelBackend
from .models import User
class EmailAuthBackend(ModelBackend):
def authenticate(self, request, email=None, password=None, **kwargs):
try:
user = User.objects.get(email=email)
except User.DoseNotExist:
return None
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
作成したバックエンドを設定に追加する
作成したバックエンドを追加するためにはmysite/settings.pyに追加します。インポートが多いのでEmailAuthenticationForm以外のクラスも表示していますが、追加するのはEmailAuthenticationFormのみです。このEmailAuthenticationFormはdjango.contrib.auth.form.AuthenticationFormをベースとしてメールアドレス用に修正しています。
mysite/settings.py
+ AUTHENTICATION_BACKENDS = [
+ 'django.contrib.auth.backends.ModelBackend',
+ 'accounts.backends.EmailAuthBackend',
+ ]
メールアドレスでのログイン用フォームを用意する
メールアドレスで認証するためのフォームを作成していきます。
accounts/forms.py
from django.contrib.auth import (
authenticate, get_user_model
)
from django.forms import ModelForm, Form
from django.forms.fields import EmailField
from django import forms
# from django.contrib.auth.models import User
# from django.contrib.auth import get_user_model
from django.contrib.auth.forms import (
AuthenticationForm, PasswordChangeForm,
PasswordResetForm, SetPasswordForm,
UserChangeForm, UserCreationForm
)
from django.utils.translation import gettext_lazy as _
from django.utils.text import capfirst
from .models import User
UserModel = get_user_model()
class UserInfoChangeForm(ModelForm):
class Meta:
model = User
fields = [
'email',
# 'last_name',
# 'first_name',
]
def __init__(self, email=None, first_name=None, last_name=None, *args, **kwargs):
kwargs.setdefault('label_suffix', '')
super().__init__(*args, **kwargs)
# ユーザーの更新前情報をフォームに挿入
if email:
self.fields['email'].widget.attrs['value'] = email
if first_name:
self.fields['first_name'].widget.attrs['value'] = first_name
if last_name:
self.fields['last_name'].widget.attrs['value'] = last_name
def update(self, user):
user.email = self.cleaned_data['email']
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
user.save()
class EmailChangeForm(ModelForm):
class Meta:
model = User
fields = ['email']
def __init__(self, email=None, *args, **kwargs):
kwargs.setdefault('label_suffix', '')
super().__init__(*args, **kwargs)
# ユーザーの更新前情報をフォームに挿入
if email:
self.fields['email'].widget.attrs['value'] = email
def update(self, user):
user.email = self.cleaned_data['email']
user.save()
class CustomAuthenticationForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
kwargs.setdefault('label_suffix', '')
super().__init__(*args, **kwargs)
class CustomPasswordChangeForm(PasswordChangeForm):
def __init__(self, *args, **kwargs):
kwargs.setdefault('label_suffix', '')
super().__init__(*args, **kwargs)
class CustomPasswordResetForm(PasswordResetForm):
def __init__(self, *args, **kwargs):
kwargs.setdefault('label_suffix', '')
super().__init__(*args, **kwargs)
class CustomSetPasswordForm(SetPasswordForm):
def __init__(self, *args, **kwargs):
kwargs.setdefault('label_suffix', '')
super().__init__(*args, **kwargs)
class AdminUserCreationForm(UserCreationForm):
class Meta:
model = User
fields = ('username', 'email')
# def save(self, commit=True):
# user = User.objects.create_user(
# self.cleaned_data["name"],
# self.cleaned_data["email"],
# self.cleaned_data["password1"],
# )
# return user
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = User
fields = '__all__'
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = User
fields = ('username', 'email')
def __init__(self, *args, **kwargs):
kwargs.setdefault('label_suffix','')
super().__init__(*args, **kwargs)
class EmailAuthenticationForm(Form):
"""
django.contrib.auth.form.AuthenticationFormをベースに改変
"""
email = EmailField(
label=_('Email'),
widget=forms.EmailInput(attrs={'autofocus': True,})
)
password = forms.CharField(
label=_("Password"),
strip=False,
widget=forms.PasswordInput,
)
error_messages = {
'invalid_login': _(
"Please enter a correct %(username)s and password. Note that both "
"fields may be case-sensitive."
),
'inactive': _("This account is inactive."),
}
def __init__(self, request=None, *args, **kwargs):
"""
The 'request' parameter is set for custom auth use by subclasses.
The form data comes in via the standard 'data' kwarg.
"""
self.request = request
self.user_cache = None
kwargs.setdefault('label_suffix', '')
super().__init__(*args, **kwargs)
# Set the max length and label for the "username" field.
self.email_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
self.fields['email'].max_length = self.email_field.max_length or 254
if self.fields['email'].label is None:
self.fields['email'].label = capfirst(self.email_field.verbose_name)
for field in self.fields.values():
field.widget.attrs["class"] = "form-control"
field.widget.attrs["placeholder"] = field.label
def clean(self):
email = self.cleaned_data.get('email')
password = self.cleaned_data.get('password')
if email is not None and password:
self.user_cache = authenticate(self.request, email=email, password=password)
if self.user_cache is None:
raise self.get_invalid_login_error()
else:
self.confirm_login_allowed(self.user_cache)
return self.cleaned_data
def confirm_login_allowed(self, user):
"""
Controls whether the given User may log in. This is a policy setting,
independent of end-user authentication. This default behavior is to
allow login by active users, and reject login by inactive users.
If the given user cannot log in, this method should raise a
``forms.ValidationError``.
If the given user may log in, this method should return None.
"""
if not user.is_active:
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',
)
def get_user(self):
return self.user_cache
def get_invalid_login_error(self):
return forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login',
params={'username': _('Email')},
)
最初の解説でも触れましたが、clean関数内でauthenticate関数を呼び認証処理を行っています。こうすることで各バックエンドを呼び出して認証することが出来ます。
ビューの変更
作成したEmailAuthenticationFormを使用するようにビューを変更しましょう。
accounts/views.py(一部抜粋)
from .forms import (
UserInfoChangeForm,
CustomAuthenticationForm, CustomPasswordChangeForm,
CustomPasswordResetForm, CustomSetPasswordForm,
CustomUserChangeForm, CustomUserCreationForm, EmailChangeForm,
+ EmailAuthenticationForm
)
#一部省略・・・
class CustomLoginView(LoginView):
- form_class = CustomAuthenticationForm
+ # form_class = CustomAuthenticationForm
+ form_class = EmailAuthenticationForm
urlに関しては変更ないですが、念の為表示しておきます。
accounts/urls.py(一部抜粋)
urlpatterns = [
path('login/', views.CustomLoginView.as_view(), name='login'),
path('logout/', views.CustomLogoutView.as_view(), name='logout'),
path('password_change/', views.CustomPasswordChangeView.as_view(), name='password_change'),
path('password_change/done/', views.CustomPasswordChangeDoneView.as_view(), name='password_change_done'),
path('password_reset/', views.CustomPasswordResetView.as_view(), name='password_reset'),
path('password_reset/done/', views.CustomPasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/<uidb64>/<token>/', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('reset/done/', views.CustomPasswordResetCompleteView.as_view(), name='password_reset_complete'),
path('create/', views.UserCreateView.as_view(), name="create"),
path('profile/', views.UserProfileView.as_view(), name="profile"),
path('change/', views.EmailChangeView.as_view(), name="change"),
]
動作確認
では動作確認をしていきましょう。これまでユーザー名でログインする画面でしたがメールアドレスでログイン出来るようになればOKです。
最後に
Djangoにおける認証の仕組みについて理解が深まれば幸いです。次回はログインユーザーのみが投稿出来るように修正していきます。