ユーザー管理機能の理解
Djangoのユーザー管理機能の概要を理解すると共に、Lunchmapアプリにどのような機能を追加するのか確認していきます。
- ユーザー情報:ログイン名やパスワードを保持する
- ユーザーの識別:ユーザーを区別し、本物かどうか確認する
- ユーザーの権限:ユーザーによって、アクセスできる機能や情報を制御する
ログイン・ログアウト画面の用意
Djangoには、あらかじめログインとログアウトなどユーザー管理用のビューが用意してあります。ここに、独自のテンプレートを追加することで、ログイン・ログアウト画面を利用できます。
ユーザ管理のルーティング
役割 | ルート |
---|---|
ログイン | accounts/login/ |
ログアウト | accounts/logout/ |
パスワード変更 | accounts/password_change/ |
パスワード変更結果 | accounts/password_change/done |
パスワードリセット | accounts/password_reset/ |
パスワードリセット結果 | accounts/password_reset/done/ |
アカウントリセット | accounts/reset/(uidb64)/(token) |
アカウントリセット結果 | accounts/reset/done/ |
プロジェクトテンプレートの配置を設定
# myapp/settings.py TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], # プロジェクト共通のテンプレートディレクトリを指定 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'myapp.wsgi.application' LOGIN_REDIRECT_URL = '/' # ログイン後にお店一覧のページへリダイレクト
ログイン・ログアウト用テンプレートのディレクトリの作成
$ cd myapp # 外側(トップ) $ mkdir templates # プロジェクト共通のテンプレートディレクトリを作成 $ cd templates $ mkdir registration # ログイン・ログアウト用テンプレートのディレクトリを作成
ルーティングを設定
# myapp/myapp/urls.py from django.contrib import admin from django.urls import path, include from django.views.generic import RedirectView urlpatterns = [ path('admin/', admin.site.urls), path('lunchmap/', include('lunchmap.urls')), # accountsにアクセスした際、ユーザ認証用のビューを呼び出す path('accounts/', include('django.contrib.auth.urls')), path('', RedirectView.as_view(url='/lunchmap/')), ]
ログイン・ログアウト画面のテンプレートを作成
独自のテンプレートを追加して、ログイン・ログアウト画面を利用できるようにしていきます。
共通テンプレートを追加
# サンプルとなるテンプレートファイルをコピー $ cd myapp $ cp lunchmap/templates/lunchmap/base.html templates/layout.html
<!-- templats/layout.html --> < !DOCTYPE html> <html> <head> <meta charset='utf-8'/> <meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'/> <link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css' integrity='sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm' crossorigin='anonymous'/> <style>body {padding-top: 80px;}</style> <title>Lunchmap</title> </head> <body> <nav class='navbar navbar-expand-sm navbar-dark bg-dark fixed-top'> Lunchmap <!-- ナビゲーションバーに、ログインとログアウトのリンクを表示 --> <ul class='navbar-nav'> <!-- ログイン中なら、ユーザ名とログアウトのリンクを表示 --> {% if user.is_authenticated %} <li class='nav-time'> <span class='navbar-text'>{{ user }} - </span> </li> <li class='nav-item'> <a href='{% url "logout" %}' class='logout nav-link'>Logout</a> </li> <!-- そうでなければログインのリンクを表示 --> {% else %} <a href='{% url "login" %}' class='login nav-link'>Login</a> {% endif %} </ul> </nav> <div class='container'> {% block content %} {% endblock %} </div> </body> </html>
ログイン用テンプレートを追加
<!-- myapp/templats/registration/login.html --> {% extends '../layout.html' %} {% block content %} <h1>Login</h1> <section class='common-form'> <!-- ログイン時のエラーメッセージを表示 --> {% if form.errors %} <p class='error-msg'>Your username and password didn't match. Please try again.</p> {% endif %} <!-- アクセス権限に関するエラーメッセージを表示 --> {% if next %} {% if user.is_authenticated %} <p class='error-msg'>Your account doesn't have access to this page. To proceed, please login with an account that has access.</p> {% else %} <p class='error-msg'>Please login to see this page.</p> {% endif %} {% endif %} <!-- ユーザ名とパスワードを入力するフォームを表示 --> <form action='{% url "login" %}' method='post'> {% csrf_token %} <input type='hidden' name='next' value='{{ next }}'/> {{ form.as_p }} <button type='submit'>Login</button> </form> </section> {% endblock %}
ログアウト用テンプレートを追加
<!-- myapp/templats/registration/logged_out.html --> {% extends '../layout.html' %} {% block content %} <h1>Logged Out</h1> <p>Thanks for spending some quality time with the Web site today.</p> <p><a href='{% url "login" %}'>Log in again</a></p> {% endblock %}
ユーザー登録画面の作成(1)
アカウント管理アプリケーションを作成して、それからこの画面を呼び出すルートを設定していきます。
accountsアプリの作成
$ python manage.py startapp accounts
accountsアプリの登録
# myapp/myapp/settings.py INSTALLED_APPS = [ 'accounts.apps.AccountsConfig', # 追加 'lunchmap.apps.LunchmapConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ]
setting.pyの言語設定の変更
# myapp/myapp/settings.py # Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ LANGUAGE_CODE = 'ja-JP' # 'en-us' # 言語設定の変更
プロジェクトのルーティングを設定
# myapp/myapp/urls.py from django.contrib import admin from django.urls import path, include from django.views.generic import RedirectView # 上から順に実行される urlpatterns = [ path('admin/', admin.site.urls), path('lunchmap/', include('lunchmap.urls')), path('accounts/', include('accounts.urls')), # accountsルートを追加 path('accounts/', include('django.contrib.auth.urls')), path('', RedirectView.as_view(url='/lunchmap/')), ]
accountsアプリのルーティングを設定
# myapp/accounts/urls.py from django.urls import path from . import views app_name = 'accounts' # accountsアプリでユーザ登録を呼び出すルートを追加 urlpatterns = [ path('signup/', views.SignUpView.as_view(), name='signup'), ]
ユーザー登録画面の作成(2)
上記で作成したAccountsアプリケーションに、ビューとテンプレートを追加して、ユーザー情報を登録できるようにしていきます。
accountsアプリのviews.pyを作成
# myapp/accounts/views.py from django.contrib.auth.forms import UserCreationForm from django.urls import reverse_lazy from django.views import generic # クラスベースの汎用ビューを使用してユーザ情報を登録 class SignUpView(generic.CreateView): form_class = UserCreationForm # UserCreationFormを呼び出す success_url = reverse_lazy('login') template_name = 'accounts/signup.html' # テンプレートしてaccounts/signup.htmlを表示
テンプレートとしてsignup.htmlを作成
<!-- myapp/accounts/templates/accounts/signup.html --> {% extends 'layout.html' %} <!-- パスを指定しない場合は、プロジェクト全体のテンプレートディレクトリを参照 --> {% block content %} <h1>Sign up</h1> <section class='common-form'> <form method='post'> {% csrf_token %} {{ form.as_p }} <!-- 汎用ビューで用意した項目を自動的にフォームとして表示 --> <button type='submit'>Sign up</button> </form> </section> {% endblock %}
共通テンプレートにサインアップを追加
<!-- myapp/templats/layout.html --> <nav class='navbar navbar-expand-sm navbar-dark bg-dark fixed-top'> Lunchmap <ul class='navbar-nav'> {% if user.is_authenticated %} <li class='nav-item'> <span class="navbar-text">{{ user }} - </span> </li> <li class='nav-item'> <a href='{% url "logout" %}' class='logout nav-link'>Logout</a> </li> {% else %} <li class='nav-item'> <a href='{% url "login" %}' class='login nav-link'>Login</a> </li> <li class="nav-item"> <!-- 共通テンプレートにSign upのリンクを追加 --> <a href='{% url "accounts:signup" %}' class='signup nav-link'> Sign up </a> </li> {% endif %} </ul> </nav>
ログイン時だけ投稿・編集・削除を可能にする
Lunchmapアプリにアクセス制御を追加していきます。
Lunchmapアプリのナビにログインを表示
<!-- myapp/lunchmap/templates/lunchmap/shop_list.html --> {% extends 'layout.html' %} <!-- 全体のテンプレートを参照するように変更 --> {% block content %}
※ 他のテンプレートも同様に修正します。
lunchmapのviews.pyにアクセス制御を追加
# myapp/lunchmap/views.py from django.urls import reverse_lazy from django.views import generic from .models import Category, Shop from django.contrib.auth.mixins import LoginRequiredMixin # ログイン時のみ利用したいクラスを継承 class IndexView(generic.ListView): model = Shop class DetailView(generic.DetailView): model = Shop class CreateView(LoginRequiredMixin, generic.edit.CreateView): # アクセス制御を追加 model = Shop fields = '__all__' class UpdateView(LoginRequiredMixin, generic.edit.UpdateView): # アクセス制御を追加 model = Shop fields = '__all__' class DeleteView(LoginRequiredMixin, generic.edit.DeleteView): # アクセス制御を追加 model = Shop success_url = reverse_lazy('lunchmap:index')
ログイン時だけ投稿・編集・削除リンクを表示
Lunchmapアプリにさらにアクセス制御を追加して、ログインしている時だけ、投稿・編集・削除リンクを表示していきます。
お店一覧ページで、ログイン時だけ「新しいお店」リンクを表示
<!-- lunchmap/shops/templates/shops/shop_list.html --> {% extends 'layout.html' %} {% block content %} <h1>お店一覧</h1> <table class='table table-striped table-hover'> <tr> <th>カテゴリ</th><th>店名</th><th>住所</th> </tr> {% for shop in object_list %} <tr> <td>{{ shop.category.name }}</td> <td><a href='{% url "lunchmap:detail" shop.pk%}'>{{ shop.name }}</a></td> <td>{{ shop.address }}</td> </tr> {% endfor %} </table> <!-- ログイン時のみリンクを表示 --> {% if user.is_authenticated %} <div> <a href='{% url "lunchmap:create" %}'>新しいお店</a> </div> {% endif %} {% endblock %}
ログイン時だけ、編集・削除リンクを表示
<!-- lunchmap/shops/templates/shops/shop_detail.html --> <div> <a href='{% url "shops:index" %}'>一覧</a> <!-- ログインIDと作成者のIDが一致している場合のみ編集と削除のリンクを表示 --> {% if request.user.id == object.author_id %} | <a href='{% url "shops:update" shop.pk %}'>編集</a> | <a href='{% url "shops:delete" shop.pk %}'>削除</a> {% endif %} </div> {% endblock %}
ログインユーザー名を作成・編集に使用
新しいお店を追加する場合、自動的にログインユーザー名で情報を登録します。
CreateViewを修正
# myapp/lunchmap/views.py class CreateView(LoginRequiredMixin, generic.edit.CreateView): model = Shop # 投稿者の名前以外をフォームとして指定 fields = ['name', 'address', 'category'] # '__all__' # form_validメソッドで格納する値をチェック def form_valid(self, form): # ログインしているユーザ名を投稿者名として代入 form.instance.author = self.request.user return super(CreateView, self).form_valid(form)
UpdateViewsで投稿者名を非表示にする
# myapp/lunchmap/views.py from django.urls import reverse_lazy from django.views import generic from .models import Category, Shop from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied # 追加 : class UpdateView(LoginRequiredMixin, generic.edit.UpdateView): model = Shop # 投稿者の名前以外をフォームとして指定 fields = ['name', 'address', 'category'] # '__all__' # dispathメソッドをオーバーライド def dispatch(self, request, *args, **kwargs): obj = self.get_object() # 投稿者名と現在のログインユーザ名が一致しなければエラーメッセージを表示 if obj.author != self.request.user: raise PermissionDenied('You do not have permission to edit.') return super(UpdateView, self).dispatch(request, *args, **kwargs)
セッションとパスワードの理解
ログイン状態を保持するセッションとユーザーを認証するパスワードについて学習していきます。
セッションとは
ログインしてから、ログアウトするまでの一連のアクセスをセッションと呼びます。
セッションという仕組みを利用すると、ログインすると開始して、そのアクセスを区別するセッション情報を記録します。そして、ログアウトしたり、ブラウザを閉じて一定時間が経ったりすると終了します。このセッション情報おかげで、セッションが有効な間、Webアプリケーションに同じ人がアクセスしていると判断できるようになります。
セッションを確認する手順
Google Chromeの場合
- 「設定」メニューを呼び出す
- 「詳細設定」-「コンテンツの設定」-「Coookies」-「すべてのクッキーとサイトデータ」
- Webアプリケーションのドメイン名を検索
パスワードの暗号化
Djangoでは、パスワードをハッシュという暗号に変換して保存していて、以下の特徴があります。
- 同じ文字列からは常に同じハッシュが作成される
- 簡単に元の文字列に戻すことができない
このハッシュ同士を比較することで、パスワードの中身を見られることなく正しいパスワードかどうかが比較できます。また、文字列をハッシュに変換することをハッシュ化といい、ハッシュ化の手法としてSHA-256やMD5などがよく知られています。