投稿日: | 最終更新日:
仮登録した後、メールから本登録させる(Django)
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時間を足した値を返します。
メールテンプレート
送信するメール本文のテンプレートです。本登録までの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の有効期限切れなどで失敗した場合に表示します。
- Python 114
- 制作 54
- RaspberryPi 41
- Django 40
- WordPress 40
- Linux 27
- VPS 22
- JavaScript 21
- PHP 20
- HTML・CSS 19
- AWS 16
- 仮想環境 15
- レスポンシブデザイン 13
- マイコン 11
- WEB全般 11
- 動画製作 9
- Webサービス 8
- 統合開発環境 8
- 機械学習 8
- PyCharm 7
- jQuery 7
- AfterEffects 7
- 起業・設立 7
- Django REST framework 6
- C# 6
- デザイン 6
- SEO 6
- pydata 6
- Visual Studio 5
- 数学 5
- 携帯サイト 5
- heroku 5
- Mac 5
- illustrator 5
- node.js 5
- Anaconda 5
- Nginx 4
- Jupyter Notebook 4
- インフラ 4
- Google Colaboratory 4
- symfony 4
- Webスクレイピング 3
- photoshop 3
- Go言語 3
- PC 3
- ツール 3
- Docker 3
- facebook 3
- 作業効率化 3
- データベース 3
- Cloud9 3
- コマンド 2
- micro:bit 2
- Kali Linux 2
- Webサーバー 2
- MariaDB 2
- ドローン 2
- コンテナ 2
- DaVinci Resolve 2
- ネットワーク 2
- Java 2
- movie 2
- PCDJ 2
- 音楽 2
- XSERVER 2
- Ansible 1
- Vue.js 1
- JSON 1
- Bootstrap 1
- バージョン管理システム 1
- SSL 1
- S3 1
- ムームードメイン 1
- ネットワーク 1
- アニメーション 1
- D3.js 1
- Rhino 1
- アニメ 1
- git 1
- windows 1
- アクセス解析 1
- スマートフォン 1
- アフィリエイトノウハウ 1
- 知識 1
- TypeScript 1
- 役立つ本・書籍 1
- データサイエンス 1
- ESP32 1
- AI 1
- ownCloud 1
- API 1