以前、DjangoでLDAP認証を試してみました。
Django + LDAP3で、ActiveDirectoryのLDAP認証によるログインとログアウトを試してみた - メモ的な思考的な
ただ、Djangoアプリ単体での認証は試したことがなかったため、今回ユーザ作成・変更・認証、およびパスワード変更やパスワードリセットを試してみました。
なお、
- できる限り、デフォルト値やDjangoで用意されている機能を使う
- できる限り、ViewはClass-based Viewを使う
という方針で実装してみます。
目次
環境
- Windows 10
- Python 3.5.1
- Django 1.9.1
準備
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_class
はDjangoで用意されている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_class
はDjangoで用意されている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
を使う- Using the Django authentication system | Django documentation | Django
- importする際に名前の重複を避けるため、
from django.contrib.auth import views as auth_views
とする
- テンプレート名はデフォルト(
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
の指定方法としては、
- ハードコーディング
- reverse_lazyによる名前付きURLパターンからURLを取得
- 名前付き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.
- ハードコーディングではなく、名前付きURLパターン(named URL patterns)を使って指定
- テンプレートが表示されずにリダイレクトされることを確認するため、
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' ),
ログアウト後にログインページへリダイレクト
今回は
- Viewは、Djangoで用意されている
django.contrib.auth.views.logout_then_login
を使う - ログインページのURLは以下の2通りの指定が可能(今回は前者を使用)
- settings.pyに
LOGIN_URL
を指定 - logout_then_loginに
login_url
を渡す
- settings.pyに
として、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と同じく
- ハードコーディング
- reverse_lazyによる名前付きURLパターンからURLを取得
- 名前付きURLパターンをそのまま書く
の3パターンですので、一番最後のパターンを使って指定します。
LOGIN_URL = 'my:login'
パスワード変更
ユーザ自身でパスワード変更ができるようにしてみます。
なお、ViewはDjangoで用意されているものを使うため、パスワード変更したいユーザで事前にログインしておく必要があります(ソースコード参照)。
今回は
- Viewは2つ用意する
- password_change viewについて
post_change_redirect
を指定しない場合、ハードコーディングされたreverse('password_change_done')
が使われるため、明示的に指定する- 該当のソースコード
- 情報元:python - Reverse for 'password_change_done' with arguments '()' and keyword arguments '{}' not found - Stack Overflow
- 指定する場合は、
reverse
ではなくreverse_lazy
を使う
- 両方のviewのtemplateについて
template_name
を指定する- Django Unleashed のp503にある通り、adminアプリとURL名前空間が重複しているようなので、デフォルトのところに自作のテンプレートを作ったとしても、adminのものが使われてしまう
として、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
- リセット用の情報入力:
- パスワード変更時と同様の対応
として、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' ),
なお、各種テンプレートのうち、ブラウザに表示するテンプレートの
- リセット用情報入力: path/to/project/myapp/templates/myapp/password_reset_form.html
- リセット用情報入力完了: path/to/project/myapp/templates/myapp/password_reset_done.html
- 新規パスワードの入力: path/to/project/myapp/templates/myapp/password_reset_confirm.html
- パスワードリセット完了: path/to/project/myapp/templates/myapp/password_reset_complete.html
については、パスワード変更と似たような内容ですので、ここでは省略して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
テーブルができており、以下のエントリがあることを確認- name: example.com
- domain: example.com
- django_siteのエントリの
domain
の値(example.com)を、localhost:8000
など、該当するホスト名へと変更
を行います。
The “sites” framework | Django documentation | Django
メールバックエンドの変更
開発環境の場合、実際にメールを送受信して確認するのが手間なので、メールのバックエンドを変更します。
Sending email | Django documentation | Django
メールのバックエンドはいくつかありますが、Console backendかFile backendを利用するため、settings.pyに追加します。
- Sending email - Console backend| Django documentation | Django
- Sending email - File backend | Django documentation | Django
なお、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を叩くとリダイレクトするものは除く)。
ユーザ作成
ユーザ変更
ログイン
ログアウト後にテンプレートを表示
パスワード変更
パスワード変更完了
パスワードリセット情報入力
パスワードリセット情報入力完了
新規パスワードの入力
パスワードリセット完了
ソースコード
GitHubに上げました。
thinkAmi-sandbox/Django_login_logout_sample
参考
Webサイト
SNS認証の方法などは以下が詳しく、参考になりました。
Djangoのユーザ認証まとめ - c-bata web
今回ユーザの無効化は行っていませんが、行う場合の指針として以下が参考になりそうです。
django - Deactivate user access or delete it? - Stack Overflow
その他には以下が参考になりました。
- Djangoを動かす(11) -認証- - 愉快な神様とヘタレな下僕
- django1.5でパスワードリセット - 雑記
- djangoのパスワードリセット機能でURLに付与されるtokenについて調べたメモ - 雑記
書籍
今回の流れは、Django Unleashed(出版社へのリンク、電子書籍あり)にも記載がありました。
- 作者: Andrew Pinkham
- 出版社/メーカー: Sams Publishing
- 発売日: 2015/11/19
- メディア: ペーパーバック
- この商品を含むブログを見る
また、書籍の公式リポジトリはこちらにありました。
jambonrose/DjangoUnleashed-1.8: Code for Django Unleashed using Django 1.8