ユーザー管理機能の理解
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などがよく知られています。
