Python Django 入門トップページ


カスタムユーザ認証

  1. プロジェクトの概要
  2. プロジェクトの作成と初期設定
  3. Users アプリケーションの作成と有効化
  4. 認証にカスタムユーザモデルを利用する
  5. モデルの作成
  6. マイグレーション
  7. ユーザの登録
  8. 管理ユーザの登録
  9. 管理サイトの作成
  10. Comments アプリケーションの作成
  11. ページ雛形の作成
  12. ログイン・ログアウトの実装
  13. Navbar の設置
  14. Comments アプリケーションのユーザ認証
  15. ユーザ一覧ページ
  16. ユーザ詳細情報の表示
  17. ユーザ情報の更新
  18. パスワードの変更
  19. Gmail 2段階認証の設定とアプリパスワードの取得
  20. メールの設定と送信
  21. パスワードのリセット
  22. ユーザ登録機能の実装
  23. ユーザ登録時に氏名も登録
  24. ユーザ登録時にメールアドレスも登録
  25. ユーザ登録してもログインできないように
  26. ユーザ登録後にメールを送信
  27. メール検証によるアカウントの有効化
  28. トークン有効期限の変更
  29. ログアウト後に top へリダイレクト
  30. 検証メールの再送信
  31. 未検証ユーザのログインエラーメッセージ
  32. メールに有効期限を表示
  33. フラッシュメッセージの変更
  34. 未検証ユーザのパスワードリセット

カスタムユーザ認証

ユーザ登録後にメールを送信

次はユーザ登録後に検証用のメールを送信する機能を実装します.ただし,メールの検証を行ってアカウントを有効化する処理は次のページで行います.

まず,送信したメールのリンクから開かれるページの URL を定義します.

users/urls.py
from django.urls import path

from . import views

app_name = 'users'
urlpatterns = [
    path('', views.users_index, name='index'),
    path('<int:user_id>/', views.users_profile, name='profile'),
    path('<int:user_id>/update/', views.users_update, name='update'),
    path('login/', views.LoginView.as_view(), name='login'),
    path('logout/', views.LogoutView.as_view(), name='logout'),
    path('password/', views.PasswordChangeView.as_view(), name='password_change_form'),
    path('password_change_done/', views.PasswordChangeDoneView.as_view(), name='password_change_done'),

    # ユーザ登録
    path('create/', views.UserCreateView.as_view(), name='create'),
    # メールリンクから呼び出される
    path('create/<uidb64>/<token>/', views.creation_confirm, name='create_confirm'),

    # パスワードリセットのメール送信画面
    path('password_reset/', views.PasswordResetView.as_view(), name='password_reset'),
    # メール送信後の画面
    path('password_reset/done/', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
    # メールリンクから呼び出される
    path('reset/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]

ユーザ登録時にメールを送信するように forms.py を修正します.

/users/forms.py
class UserCreationForm(forms.ModelForm):
    """
    A form that creates a user, with no privileges, from the given username and
    password.
    """

    error_messages = {
        "password_mismatch": _("The two password fields didn’t match."),
    }
    password1 = forms.CharField(
        label=_("Password"),
        strip=False,
        widget=forms.PasswordInput(attrs={"autocomplete": "new-password", 'class': 'form-control'}),
        help_text=password_validation.password_validators_help_text_html(),
    )
    password2 = forms.CharField(
        label=_("Password confirmation"),
        widget=forms.PasswordInput(attrs={"autocomplete": "new-password", 'class': 'form-control'}),
        strip=False,
        help_text=_("Enter the same password as before, for verification."),
    )

    class Meta:
        model = UserModel
        fields = ("username", 'first_name', 'last_name', 'email')
        field_classes = {"username": UsernameField}
        widgets = {
            'first_name': forms.TextInput(attrs={
                'class': 'form-control',
                'required' : True
            }),
            'last_name': forms.TextInput(attrs={
                'class': 'form-control',
                'required' : True
            }),
            'email': forms.EmailInput(attrs={
                "autocomplete": "email",
                'class': 'form-control',
                'required' : True
            }),
        }
        labels = {
            'first_name': 'First Name',
            'last_name': 'Last Name',
            'email': 'Email',
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self._meta.model.USERNAME_FIELD in self.fields:
            self.fields[self._meta.model.USERNAME_FIELD].widget.attrs[
                "autofocus"
            ] = True

    def clean(self):
        data = super().clean()
        first_name = data.get('first_name')
        last_name = data.get('last_name')
        if len(first_name) == 0:
            msg = "First Name が入力されていません"
            self.add_error('first_name', msg)
        elif len(first_name) > 20:
            msg = "First Name の最大文字数は20文字です"
            self.add_error('first_name', msg)
        if len(last_name) == 0:
            msg = "Last Name が入力されていません"
            self.add_error('last_name', msg)
        elif len(last_name) > 20:
            msg = "Last Name の最大文字数は20文字です"
            self.add_error('last_name', msg)

    def clean_password2(self):
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise ValidationError(
                self.error_messages["password_mismatch"],
                code="password_mismatch",
            )
        return password2

    def _post_clean(self):
        super()._post_clean()
        # Validate the password after self.instance is updated with form data
        # by super().
        password = self.cleaned_data.get("password2")
        if password:
            try:
                password_validation.validate_password(password, self.instance)
            except ValidationError as error:
                self.add_error("password2", error)

    def send_mail(
        self,
        subject_template_name,
        email_template_name,
        context,
        from_email,
        to_email,
        html_email_template_name=None,
    ):
        """
        Send a django.core.mail.EmailMultiAlternatives to `to_email`.
        """
        subject = loader.render_to_string(subject_template_name, context)
        # Email subject *must not* contain newlines
        subject = "".join(subject.splitlines())
        body = loader.render_to_string(email_template_name, context)

        email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
        if html_email_template_name is not None:
            html_email = loader.render_to_string(html_email_template_name, context)
            email_message.attach_alternative(html_email, "text/html")

        email_message.send()

    def save(
        self,
        # domain_override=None,
        subject_template_name="users/user_creation_subject.txt",
        email_template_name="users/user_creation_email.html",
        use_https=False,
        token_generator=default_token_generator,
        from_email=None,
        request=None,
        html_email_template_name=None,
        extra_email_context=None,
    ):
        user = super().save(commit=False)
        user.is_active = False  # まだログインできないようにする
        # user.is_active = True  # すぐにログインできるようにする
        user.set_password(self.cleaned_data["password1"])
        user.save()

        """
        Generate a one-use only link for user creation and send it to the
        user.
        """
        current_site = get_current_site(request)
        site_name = current_site.name
        domain = current_site.domain
        user_email = user.email
        context = {
            "email": user_email,
            "domain": domain,
            "site_name": site_name,
            "uid": urlsafe_base64_encode(force_bytes(user.pk)),
            "user": user,
            "token": token_generator.make_token(user),
            "protocol": "https" if use_https else "http",
            **(extra_email_context or {}),
        }

        # print("email : ", context["email"])
        # print("domain : ", context["domain"])
        # print("site_name : ", context["site_name"])
        # print("uid : ", context["uid"])
        # print("user : ", context["user"])
        # print("token : ", context["token"])
        # print("protocol : ", context["protocol"])

        # メールを送信する
        self.send_mail(
            subject_template_name,
            email_template_name,
            context,
            from_email,
            user_email,
            html_email_template_name=html_email_template_name,
        )

        return user

また,views.py も編集します.

users/views.py
class UserCreateView(CreateView):
    template_name = 'users/create_form.html'
    form_class = UserCreationForm

    from_email = settings.EMAIL_FROM
    email_template_name = 'users/user_creation_email.html'
    subject_template_name = 'users/user_creation_subject.txt'
    html_email_template_name = None
    extra_email_context = None
    token_generator = default_token_generator

    def form_valid(self, form):
        opts = {
            'use_https': self.request.is_secure(),
            'token_generator': self.token_generator,
            'from_email': self.from_email,
            'email_template_name': self.email_template_name,
            'subject_template_name': self.subject_template_name,
            'request': self.request,
            'html_email_template_name': self.html_email_template_name,
            'extra_email_context': self.extra_email_context,
        }
        user = form.save(**opts)
        messages.success(self.request, f'ユーザ登録しましたが,まだログインはできません {user.email}')
        return HttpResponseRedirect("/")


def creation_confirm(request, uidb64, token):
    messages.success(request, f'メールを確認しました uidb64 : {uidb64}, :token : {token}')
    return HttpResponseRedirect("/")

メールの件名と本文のテンプレートを作成します.

users/templates/users/user_creation_subject.txt
{% load i18n %}{% autoescape off %}
【{{ site_name }}】ユーザ登録
{% endautoescape %}
users/templates/users/user_creation_email.html
{% autoescape off %}

このメールは {{ site_name }} で,ユーザ登録が要求されたため送信されました.

次のリンク先のページで登録を完了してください.
{{ protocol }}://{{ domain }}{% url 'users:create_confirm' uidb64=uid token=token %}

{% endautoescape %}

受信できるメールアドレスを使ってユーザ登録を行います.受信したメールのリンクを開きます.

django2022-00347

メールのリンクから検証ページを開くことができました.メールのリンクに含まれる uidb64token を取得できていることにも注意してください.実際の検証は次のページで行います.

django2022-00348

目次に戻る