ホーム >  Python > Django >  仮登録した後、メールから本登録させる(Django)

投稿日:   |  最終更新日:

仮登録した後、メールから本登録させる(Django)

DjangoPython

Djangoでユーザ登録機能を作成します。よく会員制サイトで見かける「仮登録」→「本登録用メール送信」→「本登録」の流れを作ります。

ユーザ仮登録・本登録

前回、Userモデルをカスタマイズし、usernameを廃止してメールアドレスでユーザ登録するアプリを作成しました。今回は、ユーザ登録機能を改造して、「仮登録」→「本登録」の流れを作ります。

手順

方法は様々ですが、PHPフレームワークなどでよく使われる方法を真似します。

  • ユーザーが仮登録ページからメールアドレスとパスワードをシステムに送信(仮登録)
  • システムはトークンを発行します。
  • トークン・トークンの期限・メールアドレス・パスワードを仮登録の状態としてデータベースに保管します。
  • システムは本登録用のリンクにトークンを付加し、ユーザへメールを送信します。
  • ユーザは、メール本文中のリンクよりアクセスして本登録が完了。
  • システムは本登録が完了したという情報をデータベースに保管。
  • システムはユーザーをログインさせ、プロフィールページなどを表示します。

URL設計

トップページ(/index) ドメイン直下のページです。ログイン前はページタイトルのみですが、ログインすると、ヘッダーナビゲーションにログアウトのリンクを表示します。
ログイン(/login) ユーザ名とパスワードを入力してログインします。ログインが完了すると、ログイン状態になってトップページへ遷移します。
ログアウト(/logout) ログアウト状態にしてトップページへ遷移します。
ユーザ仮登録(/create) ユーザアカウントを仮登録をします。仮登録が完了すると、ユーザ宛にメールを送信します。
ユーザ仮登録完了(/create_done) ユーザ仮登録後表示します。
ユーザ本登録完了(/create_complete) ユーザがメール本文に記載されたURLをクリックしたときに表示します。ユーザを本登録します。本登録用URLの有効期限<が切れた場合は、有効期限切れページを表示します。
プロフィール(/profile) 本登録したユーザの情報を表示します。

仕様

①トップページです。「ドメイン/」にアクセスします。画面右上にログインページへのリンクがあります。

ログインページです。「ドメイン/login」にアクセスします。新規登録ページへのリンクもあります。

ログインページからログインした場合は「ドメイン/」(トップページ)へ遷移します。画面左上の”ログイン”が”ログアウト”になります。

ログアウトを押すと、ログアウト処理後トップページへリダイレクトします。

新規登録ページです。「ドメイン/create」にアクセスします。

新規登録が完了すると、仮登録完了ページを表示します。「ドメイン/create_done」にアクセスします。

新規登録したユーザ宛に、本登録用のメールが送信されます。

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
Subject: =?utf-8?b?44GU55m76Yyy44GC44KK44GM44Go44GG44GU44GW44GE44G+44GZ?=
From: gmailアカウント名
To: ユーザのメールアドレス
Date: Fri, xx Apr 2018 00:32:23 -0000
Message-ID: <1xxxxxx.xx01.xxxxxxxxxxxxxxx@localhost.localdomain>

ユーザ さん、以下から本登録して下さい。
http://ドメイン名:8000/create_complete/MjUzOTg1ZDFkMTEyNDMwYjhjOGI0YjA4ZWU0ZTkyOGE/

会員登録の有効期限切れ後に本登録ページを開くと、有効期限切れページを表示します。また、自動でログインが完了します。

準備

Vagrantでゲスト環境(仮想環境)を作ります。

①Userモデルをカスタマイズしています。

DjangoのUserモデルカスタマイズ(UUIDを使う)

②ユーザ登録機能を作ります。

Djangoのログイン・ユーザ登録(カスタムユーザモデル)

環境

OS CentOS 7.1.1503
pyenv 1.1.3-5-g7dae197
Anaconda 3-4.3.0
MariaDB 5.5.52-1.el7
Apache 2.4.6
mod_wsgi 4.5.14
Django 1.11.3

ユーザ仮登録概要

Userモデルの継承してカスタマイズします。usernameの代わりにemailを使うようにしています。

①以下のようなDjangoアプリ構成を作ります。プロジェクト名が「pj1」で、アプリケーション名が「users」です。プロジェクトの設定は済ませたものとします。赤字のファイルは、今回変更する部分です。

  • pj1/
  • pj1/
  • __init__.py
  • settings.py
  • urls.py
  • wsgi.py
  • users/
  • __init__.py
  • admin.py
  • apps.py
  • urls.py
  • views.py
  • models.py
  • token_manager.py
  • templates/
  • base.html
  • index.html
  • login.html
  • regist.html
  • mailtemplate/new/
  • message.txt
  • manage.py

settings.py

pj1ディレクトリ下のsettings.pyを変更します。ログイン関連のURLや、Gmailで送信する設定をしています。

LOGIN_URL = "users:login"  # ログインするページ。デフォルトにするなら"/admin/login/"等も
LOGIN_REDIRECT_URL = 'users:index'  # ログインページに直接飛んだとき、ログイン完了後のリダイレクト先

# メールを実際に送らず、コンソール画面へ表示する
#EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'gmailアカウント名'
EMAIL_HOST_PASSWORD = 'gmailパスワード'
EMAIL_USE_TLS = True

models.py

①usersディレクトリ配下のmodels.pyを編集します。内容は前回と同じです。

DjangoのUserモデルカスタマイズ(UUIDを使う)

②同じmodels.pyファイルに以下の内容を追記します。仮登録に使う1時間有効なURLを格納します。

class Activate(models.Model):
    """ 仮登録したユーザを本登録するためのModel """
 
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    key = models.CharField(max_length=255, unique=True)
    expiration_date = models.DateTimeField(blank=True, null=True)

on_delete=models.CASCADE

一対一の関係にあるテーブル(ここではUsers)が削除されると、該当のレコードが一緒に削除されます。(django2.0から必須)。デフォルトの動作です。

③makemigrationsを実行します。

python manage.py makemigrations

実行結果

Migrations for 'users':
  users/migrations/0002_activate.py

④migrateを実行します。

python manage.py migrate

実行結果

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, users
Running migrations:
  Applying users.0002_activate... OK

urls.py

①pj1ディレクトリのurls.pyを以下のように編集します。

from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url('admin/', admin.site.urls),
    url(r'^', include('users.urls', namespace = 'users'))
]

②usersディレクトリのurls.pyを以下のように編集します。

from django.conf.urls import include, url
from django.contrib.auth import views as auth_views
from . import views
 
app_name = 'users'
 
urlpatterns = [
    url(r'^$', views.index, name='index'),

    # ログイン、ログアウト
    url(r'^login/$', views.login, name='login'),
    url(r'^logout/$', views.logout, name='logout'),
    url(r'^regist/$', views.regist, name='regist'),
    url(r'^regist_save/$', views.regist_save, name='regist_save'),

    # プロフィール
    url(r'^profile$', views.profile, name='profile'),

    # 会員登録
    url(r'^create/$', views.CreateUserView.as_view(), name='create'),
    url(r'^create_done/$', views.CreateDoneView.as_view(), name='create_done'),
    url(r'^create_complete/(?P<uidb64>[0-9A-Za-z_\-]+)/$',
        views.CreateCompleteView.as_view(), name='create_complete'),

]

forms.py

①usersディレクトリ配下のforms.pyを編集します。内容は前回と同じです。

Djangoのログイン・ユーザ登録(カスタムユーザモデル)

views.py

①usersディレクトリのviews.pyを以下のように編集します。

from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.views.decorators.http import require_POST
from .forms import (
  LoginForm,
  RegisterForm,
  UserPasswordChangeForm
)
from django.contrib.auth.models import User
from django.urls import reverse_lazy
from django.views import generic
from django.contrib.auth import views as auth_views
from django.contrib.auth import get_user_model
from django.core.mail import send_mail
from django.template.loader import get_template
from django.utils.encoding import force_bytes, force_text
from django.contrib.sites.shortcuts import get_current_site
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.contrib.auth import login as auth_login
from .models import Activate
from users.backends import EmailModelBackend

#外部ファイル
from .token_manager import create_expiration_date, create_key

User = get_user_model()


def index(request):
  context = {
    'users':request.user,
  }
  return render(request, 'index.html', context)


@login_required
def profile(request):
    context = {
        'users': request.user,
    }
    return render(request, 'profile.html', context)


def login(request):
    context = {
        'template_name': 'login.html',
        'authentication_form': LoginForm
    }
    return auth_views.login(request, **context)
 

def logout(request):
    context = {
        'template_name': 'index.html',
    }
    return auth_views.logout(request, **context)


def regist(request):
  form = RegisterForm(request.POST or None)
  context = {
    'form':form,
  }
  return render(request, 'regist.html', context)


@require_POST
def regist_save(request):
    form = RegisterForm(request.POST)
    if form.is_valid():
        form.save()
        return redirect('users:index')
 
    context = {
        'form': form,
    }
    return render(request, 'regist.html', context)


class CreateUserView(generic.CreateView):
    template_name = 'create.html'
    form_class = RegisterForm
    success_url = reverse_lazy('users:create_done')

    def form_valid(self, form):
        user = form.save(commit=False)
        user.is_active = False
        user.save()

        # activeモデルの作成と保存(userモデルとひも付く)
        # uuidを使ったランダムな文字列作成
        activate_key = create_key()

        # keyの有効期限
        expiration_date = create_expiration_date()

        # 1時間有効なURLの情報レコードを作成
        activate_instance = Activate(user=user, key=activate_key, expiration_date=expiration_date)
        activate_instance.save()

        # ドメイン取得
        current_site = get_current_site(self.request)
        domain = current_site.domain

        # メール本文のテンプレート取得
        message_template = get_template('mailtemplate/new/message.txt')

        # 64ベースのエンコード
        uid = force_text(urlsafe_base64_encode(force_bytes(activate_key)))

        context = {
            'protocol': 'https' if self.request.is_secure() else 'http',
            'domain': domain,
            'uid': uid,
            'user': user,
        }

        # メール送信
        subject = 'ご登録ありがとうございます'
        message = message_template.render(context)
        from_email = settings.EMAIL_HOST_USER
        to = [user.email]
        send_mail(subject, message, from_email, to)

        result = super().form_valid(form)

        messages.success(
            self.request, '{}様宛に会員登録用のURLを記載したメールを送信しました。'.format(user.nick_name))

        return result


class CreateDoneView(generic.TemplateView):
    template_name = "create_done.html"


class CreateCompleteView(generic.TemplateView):
    template_name = 'create_complete.html'
    
    def get(self, request, **kwargs):
        uidb64 = kwargs.get("uidb64")

        try:
            #keyをデコード
            key = force_text(urlsafe_base64_decode(uidb64))
         
            # keyを使ってactivateモデルを取得
            activate = get_object_or_404(Activate, key=key)

            # activateモデルに紐付いたユーザオブジェクトを取得
            user = activate.user

            #有効期限取得
            expiration_date = activate.expiration_date
            t_now = datetime.now(timezone.utc)

        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
            user = None

        if user and not user.is_active and t_now <= expiration_date:
            context = super(CreateCompleteView, self).get_context_data(**kwargs)

            # ユーザをアクティブに変更
            user.is_active = True
            user.save()

            # 本登録後、ログイン処理
            user.backend = 'users.backends.EmailModelBackend'
            auth_login(request, user)

            # ページに表示するメッセージ
            response_message = "本登録が完了しました。"
            context["message"] = response_message

            # 仮登録用トークン削除
            Activate.objects.filter(key=key).delete()

            return render(self.request, self.template_name, context)
        else:

            # 仮登録用トークン削除
            Activate.objects.filter(key=key).delete()

            if user :
                # 仮登録ユーザ削除
                User.objects.filter(uuid=user.uuid).delete()

            return render(request, 'create_failed.html')

解説

from django.contrib.auth import login as auth_login

login関数をインポートします。そのまま”login”でインポートしてしまうと、いかのようなエラーが出てしまします。”as auth_login”のように、独自に名前を定義しましょう。

login() takes 1 positional argument but 2 were given

User = get_user_model()

現在アクティブなユーザーモデル(カスタムユーザーモデル)を返し、”User”に設定します。

class CreateUserView(generic.CreateView):
    template_name = 'create.html'
    form_class = RegisterForm
    success_url = reverse_lazy('users:create_done')

ユーザモデルにユーザを登録するクラスです。forms.pyで定義した「RegisterForm」を読み込みます。登録が完了したら「create_done」へリダイレクトします。

    def form_valid(self, form):
        user = form.save(commit=False)
        user.is_active = False
        user.save()

ユーザモデルにユーザを登録します。仮登録の段階なので、is_activeをFalseにします。

        # activeモデルの作成と保存(userモデルとひも付く)
        # uuidを使ったランダムな文字列作成
        activate_key = create_key()
 
        # keyの有効期限
        expiration_date = create_expiration_date()

        # 1時間有効なURLの情報レコードを作成
        activate_instance = Activate(user=user, key=activate_key, expiration_date=expiration_date)
        activate_instance.save()

仮登録の認証に使う1時間有効なURLを格納するレコードを作ります。

        # ドメイン取得
        current_site = get_current_site(self.request)
        domain = current_site.domain

プロトコルやドメインを取得しています。

        # メール本文のテンプレート取得
        message_template = get_template('mailtemplate/new/message.txt')

メッセージのテンプレートを読み込みます。

        # 64ベースのエンコード
        uid = force_text(urlsafe_base64_encode(force_bytes(activate_key)))
 
        context = {
            'protocol': 'https' if self.request.is_secure() else 'http',
            'domain': domain,
            'uid': uid,
            'user': user,
        }

activeモデルのキー(uuid)から、本登録用URLを作ります。キーをURLでの使用のためにバイトコードをbase64でエンコードし、末尾の等号をすべて取り除きます。

        # メール送信
        subject = 'ご登録ありがとうございます'
        message = message_template.render(context)
        from_email = settings.EMAIL_HOST_USER
        to = [user.email]
        send_mail(subject, message, from_email, to)

件名、本文、宛先を設定してメッセージを送信します。

        result = super().form_valid(form)

        messages.success(
            self.request, '{}様宛に会員登録用のURLを記載したメールを送信しました。'.format(user.nick_name))

        return result

リダイレクト先で、メール送信完了のメッセージを表示します。

class CreateDoneView(generic.TemplateView):
    template_name = "create_done.html"

メール送信完了ページを表示します。

class CreateCompleteView(generic.TemplateView):
    template_name = 'create_complete.html'

本登録ページを表示します。TemplateViewを使い、getメソッドを上書きし、そのURLが正しいかを確認しています。

    def get(self, request, **kwargs):
        uidb64 = kwargs.get("uidb64")

本登録用URLからキーを取得します。

            #keyをデコード
            key = force_text(urlsafe_base64_decode(uidb64))

取得したキーはエンコードしましたので、元のUUIDにデコードします。

            # keyを使ってactivateモデルを取得
            activate = get_object_or_404(Activate, key=key)
 
            # activateモデルに紐付いたユーザオブジェクトを取得
            user = activate.user
 
            #有効期限取得
            expiration_date = activate.expiration_date
            t_now = datetime.now(timezone.utc)

取得したキーはエンコードしましたので、元のUUIDにデコードします。

        if user and not user.is_active and t_now <= expiration_date:
            context = super(CreateCompleteView, self).get_context_data(**kwargs)
 
            # ユーザをアクティブに変更
            user.is_active = True
            user.save()
 
            # 本登録後、ログイン処理
            user.backend = 'users.backends.EmailModelBackend'
            auth_login(request, user)
 
            # ページに表示するメッセージ
            response_message = "本登録が完了しました。"
            context["message"] = response_message
 
            # 仮登録用トークン削除
            Activate.objects.filter(key=key).delete()
 
            return render(self.request, self.template_name, context)
        else:
 
            # 仮登録用トークン削除
            Activate.objects.filter(key=key).delete()
 
            if user :
                # 仮登録ユーザ削除
                User.objects.filter(uuid=user.uuid).delete()
 
            return render(request, 'create_failed.html')

メール本文のリンクから来たか確認したらuser.is_activeをTrueにし、有効化します。

token_manager.py

①token_manager.pyは、仮登録で使用する関数を外出しした外部ファイルです。usersディレクトリに新たにtoken_manager.pyを作成し、以下のように記述します。

# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import uuid
from uuid import uuid4
from django.utils.encoding import force_bytes, force_text
import datetime

def create_key():
    """ ランダムな文字列を生成 """
 
    key = uuid.uuid4().hex
    return key

def create_expiration_date():
    """ 仮登録の有効期限を生成 """

    now = datetime.datetime.now()

    # 現日時に1時間 加算
    expiration_date = now + datetime.timedelta(hours=1)

    return expiration_date

解説

def create_key():
    """ ランダムな文字列を生成 """
 
    key = uuid.uuid4().hex
    return key

本登録用URLのキーを作成します。「.hex」は、32文字の16進数文字列でのUUIDを作成します。

def create_expiration_date():
    """ 仮登録の有効期限を生成 """

    now = datetime.datetime.now()

    # 現日時に1時間 加算
    expiration_date = now + datetime.timedelta(hours=1)

    return expiration_date

キーの有効期限日時を作成します。現在に1時間を足した値を返します。

メールテンプレート

{{ user.nick_name }} さん、以下から本登録して
{{ protocol}}://{{ domain }}{% url 'users:create_complete' uidb64=uid %}

送信するメール本文のテンプレートです。本登録までのURLと、keyのパラメータ(uidb64)を表示します。

テンプレート

「Base.html」、「index.html」、「login.html」は、前回の通りです。

{% extends "base.html" %}
{% block content %}
<div class="container-fluid" style="margin-top: 60px;">
  <div class="row">
    <div class="col-md-6 offset-md-3">
      <div class="card">
        <div class="card-header">会員登録用URL送信</div>
          <div class="card-body">
            <div class="card-block">
              <form class="my-form" action="" method="POST">
              {% csrf_token %}
                <div class="row">
                  <div class="form-group col-md-11">
                    <label for="id_nick_name">お名前</label>
                    {{ form.nick_name }}
                    {{ form.nick_name.errors }}
                  </div>
                  <div class="form-group col-md-11 my-0">
                    <label for="id_email">メールアドレス</label>
                    {{ form.email }}
                    {{ form.email.errors }}
                    <p class="help-block">{{ form.email.help_text }}</p>
                  </div>
                  <div class="form-group col-md-11">
                    <label for="id_password1">パスワード</label>
                    {{ form.password1 }}
                    {{ form.password1.errors }}
                  </div>
                  <div class="form-group col-md-11">
                    <label for="id_password2">パスワード(確認用)</label>
                    {{ form.password2 }}
                    {{ form.password2.errors }}
                    <p class="help-block">{{ form.password2.help_text }}</p>
                  </div>
                  <div class="form-group col-md-11">
                    <input type='hidden' name='next' value='{{ next }}' />
                    <button type="submit" class="btn btn-block btn-outline-primary">メールを送信する</button>
                  </div>
                </div>
              </form>
            </div><!-- end card-block -->
          </div><!-- end card-body -->
      </div><!-- end card -->
    </div><!-- end col-md-6 offset-md-3 -->
  </div><!-- end row -->
</div><!-- end container-fluid -->
{% endblock %}

「create.html」は、ユーザ仮登録画面を定義します。

{% extends 'base.html' %}
{% block content %}
<div class="container-fluid" style="margin-top: 60px;">
  <div class="row">
    <div class="col-md-6 offset-md-3">
      <div class="card">
        <div class="card-header">メールの送信</div>
        <div class="card-body p-3">
          <p class="card-text">
            会員登録ありがとうございます。太郎様宛に、会員登録用のURLを記載したメールを送信しました。
            </p>
          <div class="alert alert-danger" role="alert">現時点では会員登録は完了していません。</div>
           <p class="card-text">メールをご確認の上、メール本文中のURLをクリックし、本登録を行って下さい。</p>
        </div><!-- end card-body-->
      </div><!-- end card-->
    </div><!-- end col-md-6 offset-md-3 -->
  </div><!-- end row -->
</div><!-- end container-fluid-->
{% endblock %}

「create_done.html」は、ユーザ仮登録完了(メール送信完了)画面を定義します。

{% extends 'base.html' %}
{% block content %}
<div class="container-fluid" style="margin-top: 60px;">
  <div class="row">
    <div class="col-md-6 offset-md-3">
      <div class="card">
        <div class="card-header">会員登録完了</div>
        <div class="card-body p-3">
          <p class="card-text">お客様の会員登録が完了しました。プロフィールページにお進み下さい。</p>
          <a href="{% url 'users:profile' %}" class="btn btn-primary">Go Your Profiles</a>
        </div><!-- end card-body-->
      </div><!-- end card-->
    </div>
  </div><!-- end row -->
</div><!-- end container-fluid-->
{% endblock %}

「create_done.html」は、ユーザ仮登録完了(メール送信完了)画面を定義します。

{% extends 'base.html' %}
{% block content %}
<div class="container-fluid" style="margin-top: 60px;">
  <div class="row">
    <div class="col-md-6 offset-md-3">
      <div class="card">
        <div class="card-header">会員登録失敗</div>
        <div class="card-body p-3">
          <p class="card-text">
            会員登録用URLの有効期限がきれました。お手数をお掛けしますが、再度会員登録を行って下さい。</p>
          <a href="{% url 'users:create' %}" class="btn btn-primary">Go To The Create Account Page</a>
        </div><!-- end card-body-->
      </div><!-- end card-->
    </div>
  </div><!-- end row -->
</div><!-- end container-fluid-->
{% endblock %}

「create_failed.html」は、ユーザ本登録がURLの有効期限切れなどで失敗した場合に表示します。

トラックバック用のURL
プロフィール

名前:イワサキ ユウタ 職業:システムエンジニア、ウェブマスター、フロントエンドエンジニア 誕生:1986年生まれ 出身:静岡県 特技:ウッドベース 略歴 20

最近の投稿
人気記事
カテゴリー
広告