読者です 読者をやめる 読者になる 読者になる

Djangoで、Djangoアプリ単体でのユーザ作成・変更・認証・パスワード変更・パスワードリセットを試してみた

Django Python

以前、DjangoLDAP認証を試してみました。
Django + LDAP3で、ActiveDirectoryのLDAP認証によるログインとログアウトを試してみた - メモ的な思考的な

 
ただ、Djangoアプリ単体での認証は試したことがなかったため、今回ユーザ作成・変更・認証、およびパスワード変更やパスワードリセットを試してみました。

なお、

  • できる限り、デフォルト値やDjangoで用意されている機能を使う
  • できる限り、ViewはClass-based Viewを使う

という方針で実装してみます。

 

環境

 

準備

d:\Sandbox\Django_login_logout_sample>virtualenv -p c:\python35-32\python.exe env
d:\Sandbox\Django_login_logout_sample>env\Scripts\activate
(env) d:\Sandbox\Django_login_logout_sample>pip install django
(env) d:\Sandbox\Django_login_logout_sample>django-admin startproject myproject .
(env) d:\Sandbox\Django_login_logout_sample>python manage.py startapp myapp

 

ユーザ登録

本来はユーザ登録機能の実行制限をかけた方が良いかと思いますが、今回は誰でもユーザを登録できるようにしました。

  • generic class-based viewのCreateViewを使う
  • form_classDjangoで用意されているUserCreationFormを使う
  • テンプレート名はデフォルト(templates/auth/user_form.html)とするため、template_nameを指定しない
    • この場合、modelを指定しないと以下のエラーとなるため、django.contrib.auth.models.Userをmodelに指定する
      • TemplateResponseMixin requires either a definition of 'template_name' or an implementation of 'get_template_names()'
  • 登録後のリダイレクト先は、再度ユーザ登録画面
    • get_success_url()にて指定

として、views.py、urls.py、テンプレートを実装します。

 

path/to/project/myapp/views.py
class AccountCreateView(CreateView):
    model = User
    
    form_class = UserCreationForm
    
    def get_success_url(self):
        return reverse('my:user_creation')
path/to/project/myapp/urls.py
url(r'^user-creation/$', views.AccountCreateView.as_view(), name='user_creation'),
path/to/project/myapp/templates/auth/user_form.html
{# 新規登録時はobject(=model)のIDが登録されていないはず #}
{% if object.id %}
    <h1>Update User</h1>
{% else %}
    <h1>Create User</h1>
{% endif %}

<div id="main">
    <form method="post" action="">
        <fieldset>
            {% csrf_token %}
            
            {{ form.as_p }}
            
            <input type="submit" id="save" value="Save">
        </fieldset>
    </form>
</div>

 

ユーザ変更

今回は

  • generic class-based viewのUpdateViewを使う
  • form_classDjangoで用意されているUserChangeFormを使う
  • テンプレート名はデフォルト(templates/auth/user_form.html)のため、template_nameは指定しない
    • この場合、modelを指定しないと以下のエラーとなるため、django.contrib.auth.models.Userをmodelに指定する
      • AccountUpdateView is missing a QuerySet. Define AccountUpdateView.model, AccountUpdateView.queryset, or override AccountUpdateView.get_queryset().
  • 登録後のリダイレクト先は、再度同じユーザ変更画面
    • get_success_url()にて指定

として、views.pyとurls.pyを実装します。

なお、CreateViewとUpdateViewにおけるデフォルトテンプレート名は同一のため、ユーザ変更ではテンプレートを用意しません。
Form handling with class-based views | Django documentation | Django

 

path/to/project/myapp/views.py
class AccountUpdateView(UpdateView):
    model = User
    form_class = UserChangeForm
    
    def get_success_url(self):
        return reverse('my:user_change', args=(self.object.id, ))
path/to/project/myapp/urls.py
url(r'^(?P<pk>[0-9]+)/update/$', views.AccountUpdateView.as_view(), name='user_change'),  

 

ユーザ認証(ログイン)

今回は

  • Viewは、Djangoで用意されているdjango.contrib.auth.views.loginを使う
  • テンプレート名はデフォルト(templates/registration/login.html)のまま
  • ログイン後のリダイレクト先として、settings.pyにLOGIN_REDIRECT_URLを設定

として、urls.pyとテンプレート、settings.pyを実装します。

path/to/project/myapp/urls.py
url(r'^login/$', auth_views.login, name='login'),
path/to/project/myapp/templates/registration/login.html
<h1>login page</h1>

<div id="main">

    {% if form.errors %}
    <p>Your username and password didn't match. Please try again.</p>
    {% endif %}

    <form method="post" action="">
        {% csrf_token %}
        <table>
            <tr>
                <td>{{ form.username.label_tag }}</td>
                <td>{{ form.username }}</td>
            </tr>
            <tr>
                <td>{{ form.password.label_tag }}</td>
                <td>{{ form.password }}</td>
            </tr>
        </table>

        <input type="submit" value="login" />
        <input type="hidden" name="next" value="{{ next }}" />
    </form>
</div>

 

path/to/project/myproject/settings.py

LOGIN_REDIRECT_URLの指定方法としては、

の3つが考えられましたが、最後のものが簡潔だったので、それを使いました。

LOGIN_REDIRECT_URL = 'my:index'

 

ログアウト

ログアウトした後の挙動として、

  • テンプレートを表示
  • 指定したページにリダイレクト
  • ログインページにリダイレクト

が考えられました。

Djangoではいずれも準備されていたため、試してみることにします。

 

ログアウト後にテンプレートを表示

今回は

  • Viewは、Djangoで用意されているdjango.contrib.auth.views.logoutを使う
  • テンプレートはデフォルト名(templates/registration/logged_out.html)とは異なる、templates/myapp/logged_out.html
    • デフォルト名のテンプレートを作成しても、Django adminのテンプレートが表示されてしまったため、明示的にテンプレート名を指定

として、urls.pyとテンプレートを実装します。

path/to/project/myapp/urls.py

viewにtemplate_nameパラメータを渡すため、url関数の第三引数にディクショナリを渡します。
URL dispatcher | Django documentation | Django

url(r'^logout-with-template/$', 
    auth_views.logout,
    {
        'template_name': 'myapp/logged_out.html',
    },
    name='logout_with_template'
),
path/to/project/myapp/templates/myapp/logged_out.html
<h1>logged out page</h1>

<div id="main">
    <p>logged out</p>
</div>

 

ログアウト後に指定したページヘリダイレクト

今回は

  • Viewは、Djangoで用意されているdjango.contrib.auth.views.logoutを使う
  • next_pageでリダイレクト先を指定
    • ハードコーディングではなく、名前付きURLパターン(named URL patterns)を使って指定
      • ハードコーディング('next_page': 'mysite/')すると、http://localhost:8000/mysite/logout/mysite/へリダイレクト
    • 名前付きURLパターンからURLを取得するために、django.core.urlresolvers.reverse_lazyを使う
      • django.core.urlresolvers.reverseでは、以下のエラー
        • django.core.exceptions.ImproperlyConfigured: The included URLconf 'myproject.urls' does not appear to have any patterns in it. If you see valid patterns in the file then the issue is probably caused by a circular import.
  • テンプレートが表示されずにリダイレクトされることを確認するため、template_nameも指定
    • 本来不要

として、urls.pyのみ実装します。

path/to/project/myapp/urls.py
url(r'^logout-with-redirect/$', 
    auth_views.logout,
    {
        'next_page': reverse_lazy('my:index'),
        'template_name': 'myapp/logged_out.html',
    },
    name='logout_with_redirect'
),

 

ログアウト後にログインページへリダイレクト

今回は

として、urls.pyとsettings.pyを実装します。

path/to/project/myapp/urls.py
url(r'^logout-then-login/$', 
    auth_views.logout_then_login,
    {
        # 今回はsettings.pyで`LOGIN_URL`を指定したので、ここはコメントアウト
        # 'login_url': reverse_lazy('my:login'),
    },
    name='logout_then_login'
),

 

path/to/project/myproject/settings.py

LOGIN_URLの指定方法としては、LOGIN_REDIRECT_URLと同じく

の3パターンですので、一番最後のパターンを使って指定します。

LOGIN_URL = 'my:login'

 

パスワード変更

ユーザ自身でパスワード変更ができるようにしてみます。

なお、ViewはDjangoで用意されているものを使うため、パスワード変更したいユーザで事前にログインしておく必要があります(ソースコード参照)。

今回は

として、urls.pyと2つのテンプレート(password_change_form.html, password_change_done.html)を実装します。

path/to/project/myapp/urls.py

ユーザ変更のHTMLから「but you can change the password using this form.」としてリンクされているURLは<user_id>/passwordになりますが、今回は対応しません。

url(r'^password-change/$', 
    auth_views.password_change, 
    {
        'post_change_redirect': reverse_lazy('my:pwd_change_done'),
        'template_name': 'myapp/password_change_form.html',
    },
    name='pwd_change'
),

url(r'^password-change-done/$', 
    auth_views.password_change_done, 
    {
        'template_name': 'myapp/password_change_done.html',
    },
    name='pwd_change_done'
),
path/to/project/myapp/templates/myapp/password_change_form.html
<h1>Password Change</h1>
<div id="main">
    <form method="post" action="">
        <fieldset>
            {% csrf_token %}
            
            {{ form.as_p }}
            
            <input type="submit" id="save" value="Save">
        </fieldset>
    </form>
</div>
path/to/project/myapp/templates/myapp/password_change_done.html
<h1>Password Change done</h1>
<div id="main">
    <p>done.</p>
</div>

 

パスワードリセット

パスワードを忘れた場合に使われるパスワードリセット機能もDjangoで用意されています。
Using the Django authentication system | Django documentation | Django

なお、パスワードリセット時は

  • ユーザにメールアドレスを登録済
  • 入力したメールアドレスとそのアドレスが一致

をともに満たした場合のみ、メール送信が行われます。

そのため、テストする場合には事前にメールアドレスを登録しておきます。

 

Viewと各種テンプレートの準備

今回は

  • 使用するDjangoのViewは4つ
    • リセット用の情報入力: django.contrib.auth.views.password_reset
    • リセット用の情報入力完了: django.contrib.auth.views.password_reset_done
      • 裏側で、リセット画面のURLが入ったメールが送信される
    • 新規パスワードの入力: django.contrib.auth.views.password_reset_confirm
    • パスワードリセット完了: django.contrib.auth.views.password_reset_complete
  • パスワード変更時と同様の対応
    • テンプレート名を指定しないと、adminアプリとURL名前空間が重複
    • post_reset_redirectなどのリダイレクト先を指定するパラメータは、デフォルトのままだと reverse('password_reset_done') されてしまう

として、urls.pyと各種テンプレートを用意します。

 
なお、リセット用の情報入力(password_reset)のformをデフォルト(PasswordResetForm)とするとメールアドレス入力のみでのリセットとなります。

これにより、メールアドレスが重複しているユーザが存在した場合、望んだ形でのパスワードリセットができませんでした。

そのため、実際に使うときには

  • ユーザ登録・変更時、他のユーザと重複しているメールアドレスの登録は許可しない
  • PasswordResetFormを継承して、ユーザ名とメールアドレスの組み合わせでリセットする

などの対策を取ったほうがいいのかなと感じました。

 

path/to/project/myapp/urls.py
# リセット用の情報入力
url(r'^password-reset/$',
    auth_views.password_reset,
    {
        'post_reset_redirect': reverse_lazy('my:pwd_reset_done'),
        'template_name': 'myapp/password_reset_form.html',
        'email_template_name': 'myapp/password_reset_email.html',
        'subject_template_name': 'myapp/password_reset_subject.txt',
    },
    name='pwd_reset'
),
# リセット用の情報入力完了
url(r'^password-reset-done/$',
    auth_views.password_reset_done,
    {
        'template_name': 'myapp/password_reset_done.html',
    },
    name='pwd_reset_done'
),
# 新規パスワードの入力
url(r'^password-reset-confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>.+)/$',
    auth_views.password_reset_confirm,
    {
        'template_name': 'myapp/password_reset_confirm.html',
        'post_reset_redirect': reverse_lazy('my:pwd_reset_complete'),
    },
    name='pwd_reset_confirm'
),
# パスワードリセット完了
url(r'^password-reset-complete/$',
    auth_views.password_reset_complete,
    {
        'template_name': 'myapp/password_reset_complete.html',
    },
    name='pwd_reset_complete'
),

 
なお、各種テンプレートのうち、ブラウザに表示するテンプレートの

については、パスワード変更と似たような内容ですので、ここでは省略してGitHubへのリンクとしておきます。

 
他に、以下のメール用のテンプレートを用意します。

path/to/project/myapp/templates/myapp/password_reset_subject.txt

メールの件名のテンプレートです。

Reset Password

 

path/to/project/myapp/templates/myapp/password_reset_email.html

メール本文のテンプレートです。

このテンプレートでは名前付きURLパターンなどを指定できます。

Someone asked for password reset for email {{ email }}. Follow the link below:
{{ protocol}}://{{ domain }}{% url 'my:pwd_reset_confirm' uidb64=uid token=token %}

 

Django sites frameworkの追加

メールのテンプレートにある {{ domain }} の値は、Django sites frameworkを使って設定します。
The “sites” framework | Django documentation | Django

sites frameworkを使うため、

  • settigns.pyのINSTALL_APPSにdjango.contrib.sitesを追加
  • settings.pyに、SITE_ID = 1 を追加
  • python manage.py migrate を実行
  • データベースにdjango_siteテーブルができており、以下のエントリがあることを確認
  • django_siteのエントリのdomainの値(example.com)を、localhost:8000など、該当するホスト名へと変更

を行います。
The “sites” framework | Django documentation | Django

 

メールバックエンドの変更

開発環境の場合、実際にメールを送受信して確認するのが手間なので、メールのバックエンドを変更します。
Sending email | Django documentation | Django

メールのバックエンドはいくつかありますが、Console backendかFile backendを利用するため、settings.pyに追加します。

なお、Dummy backendもありますが、今回はメール本文に記載されたURLが必要なので使えません。

 

myproject/settings.py
# コンソール(コマンドプロンプト)の場合
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# ファイル出力の場合
EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
EMAIL_FILE_PATH = r'D:/dev/sandbox/django_login/email'

 
ちなみに、コマンドプロンプトへメールが出力された場合、以下のように表示されます。

MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Subject: Reset Password
From: webmaster@localhost
To: hoge@example.com
Date: Sat, 23 Jan 2016 14:26:02 -0000
Message-ID: <your_id>

Someone asked for password reset for email hoge@example.com. Follow the link below:
http://localhost:8000/mysite/password-reset-confirm/MQ/48s-647f46eb15f8e88230dd/

 

動作確認

スクリーンショットは以下の通りです(URLを叩くとリダイレクトするものは除く)。

ユーザ作成

f:id:thinkAmi:20160123233130p:plain

ユーザ変更

f:id:thinkAmi:20160123233145p:plain

ログイン

f:id:thinkAmi:20160123233201p:plain

ログアウト後にテンプレートを表示

f:id:thinkAmi:20160123233215p:plain

パスワード変更

f:id:thinkAmi:20160123233333p:plain

パスワード変更完了

f:id:thinkAmi:20160123233350p:plain

パスワードリセット情報入力

f:id:thinkAmi:20160123233404p:plain

パスワードリセット情報入力完了

f:id:thinkAmi:20160123233433p:plain

新規パスワードの入力

f:id:thinkAmi:20160123233449p:plain

パスワードリセット完了

f:id:thinkAmi:20160123233502p:plain

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/Django_login_logout_sample

 

参考

Webサイト

SNS認証の方法などは以下が詳しく、参考になりました。
Djangoのユーザ認証まとめ - c-bata web

今回ユーザの無効化は行っていませんが、行う場合の指針として以下が参考になりそうです。
django - Deactivate user access or delete it? - Stack Overflow

その他には以下が参考になりました。

 

書籍

今回の流れは、Django Unleashed(出版社へのリンク、電子書籍あり)にも記載がありました。

Django Unleashed

Django Unleashed

また、書籍の公式リポジトリはこちらにありました。
jambonrose/DjangoUnleashed-1.8: Code for Django Unleashed using Django 1.8