18e. Django入門5(Djangoでユーザー管理)

ユーザー管理機能の理解

Djangoのユーザー管理機能の概要を理解すると共に、Lunchmapアプリにどのような機能を追加するのか確認していきます。

  1. ユーザー情報:ログイン名やパスワードを保持する
  2. ユーザーの識別:ユーザーを区別し、本物かどうか確認する
  3. ユーザーの権限:ユーザーによって、アクセスできる機能や情報を制御する

ログイン・ログアウト画面の用意

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'>
            <a class='navbar-brand' href='{% url "lunchmap:index" %}'>Lunchmap</a>
            <!-- ナビゲーションバーに、ログインとログアウトのリンクを表示 -->
            <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'>
    <a class='navbar-brand' href='{% url "lunchmap:index" %}'>Lunchmap</a>
    <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の場合

  1. 「設定」メニューを呼び出す
  2. 「詳細設定」-「コンテンツの設定」-「Coookies」-「すべてのクッキーとサイトデータ」
  3. Webアプリケーションのドメイン名を検索

 

パスワードの暗号化

Djangoでは、パスワードをハッシュという暗号に変換して保存していて、以下の特徴があります。

  1. 同じ文字列からは常に同じハッシュが作成される
  2. 簡単に元の文字列に戻すことができない

このハッシュ同士を比較することで、パスワードの中身を見られることなく正しいパスワードかどうかが比較できます。また、文字列をハッシュに変換することをハッシュ化といい、ハッシュ化の手法としてSHA-256やMD5などがよく知られています。

Webプログラミング入門

タイトルとURLをコピーしました