A Complete Beginner's Guide to Djangoのチュートリアルを参考にユーザー認証を実装してみる。
ユーザー認証
Djangoの認証システムを使用して下記機能が使えるユーザー認証を実装していく。
なお、チュートリアルはDjango 1.11で書かれているためDjango2.0ではもっと良い実装方法があるかもしれないが、まずはチュートリアル通りに実装してみる。
- サインアップ(ユーザー登録)
- ログイン
- ログアウト
- パスワードリセット
- パスワード変更
accountsアプリ作成
掲示板アプリ用にboardsというアプリを作成しているが、ユーザー認証用のアプリを下記コマンドで作成する。
$ django-admin startapp accounts
ディレクトリ構成は以下のようになる。
├── myproject/ │ ├── accounts/ # 新しいユーザ認証アプリ │ ├── boards/ │ ├── db.sqlite3 │ ├── manage.py │ ├── myproject/ │ ├── static/ ├── Pipfile └── Pipfile.lock
アプリを有効にするためにsettings.pyのINSTALLED_APPSに追加する。
INSTALLED_APPS = [
'accounts.apps.AccountsConfig',
'boards.apps.BoardsConfig',
...
]
なお、認証に関するアプリ、ミドルウェアはデフォルトで有効になっているので特にsettings.pyを変更する必要はない。
サインアップページ作成
サインアップページを作成するためにmyproject/urls.pyにsignup/の行を追加する。
from django.contrib import admin from django.conf import settings from django.urls import path, include from accounts import views as accounts_views from boards import views urlpatterns = [ path('', views.home, name='home'), path('signup/', accounts_views.signup, name='signup'), path('boards/<int:pk>/', views.board_topics, name='board_topics'), path('boards/<int:pk>/new/', views.new_topic, name='new_topic'), path('admin/', admin.site.urls), ]
from accounts import views as accounts_viewsのように別名を付けてimportしているが、これはboardと衝突しないようにするため(このurls.pyの書き方が悪いようなので後のチュートリアルで改善するとのこと)。
次にaccounts/views.pyを編集する。
from django.shortcuts import render def signup(request): return render(request, 'signup.html')
テンプレートをレンダリングするだけの処理で、表示するtemplates/signup.htmlを作成する。
{% extends 'base.html' %}
{% block content %}
<h2>Sign up</h2>
{% endblock %}
signup/にアクセスして表示されるか確認する。

追加したサインアップ用のテストをaccounts/tests.pyに追加する。
from django.urls import reverse, resolve from django.test import TestCase from .views import signup class SignUpTests(TestCase): def test_signup_status_code(self): url = reverse('signup') response = self.client.get(url) self.assertEquals(response.status_code, 200) def test_signup_url_resolves_signup_view(self): view = resolve('/signup/') self.assertEquals(view.func, signup)
テストを実行して通ることを確認する。
$ python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). .................. ---------------------------------------------------------------------- Ran 18 tests in 1.370s OK Destroying test database for alias 'default'...
ユーザー認証のページではパンくずリストは使わないので、パンくずリストを表示しているtemplates/base.htmlを変更する(<!-- HERE -->の部分)。
{% load static %}<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}Django Boards{% endblock %}</title>
<link href="https://fonts.googleapis.com/css?family=Pacifico" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/app.css' %}">
{% block stylesheet %}{% endblock %} <!-- HERE -->
</head>
<body>
{% block body %} <!-- HERE -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'home' %}">Django Boards</a>
</div>
</nav>
<div class="container">
<ol class="breadcrumb my-4">
{% block breadcrumb %}
{% endblock %}
</ol>
{% block content %}
{% endblock %}
</div>
{% endblock body %} <!-- AND HERE -->
</body>
</html>
{% block stylesheet %}{% endblock %}はCSSを追加できるようにするためのもの。
{% block body %}は{% block content %}より大きな<body>全体を含む範囲のため、全体を置き換えることができる。また、{% endblock body %}のように閉じるブロックに名前を付けるとコードが見やすくなる。
templates/signup.htmlを編集し、{% block content %}から{% block body %}に変える。
{% extends 'base.html' %}
{% block body %}
<h2>Sign up</h2>
{% endblock %}
これでパンくずリストが表示されなくなった。

フォーム作成
サインアップフォームを作成するためにDjangoビルトインのUserCreationFormを使用する。
accounts/views.pyを以下のように編集。
from django.contrib.auth.forms import UserCreationForm from django.shortcuts import render def signup(request): form = UserCreationForm() return render(request, 'signup.html', {'form': form})
続いてtemplates/signup.htmlを以下のように編集する。
{% extends 'base.html' %}
{% block body %}
<div class="container">
<h2>Sign up</h2>
<form method="post" novalidate>
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Create an account</button>
</form>
</div>
{% endblock %}
ページにアクセスするとフォームが表示される。
UserCreationFormはusername, password1, password2の3つのフィールドがある。

Bootstrapが適用されていないのでtemplates/signup.htmlを編集し、前回作成したform.htmlをincludeする。
{% extends 'base.html' %}
{% block body %}
<div class="container">
<h2>Sign up</h2>
<form method="post" novalidate>
{% csrf_token %}
{% include 'includes/form.html' %}
<button type="submit" class="btn btn-primary">Create an account</button>
</form>
</div>
{% endblock %}

Bootstrapが適用されたが、パスワードの説明に<ul>などのHTMLタグが出力されてしまっている。
これはDjangoがデフォルトでは問題になりそうな特殊文字をエスケープしているためだが、今回のような問題のない説明文に対してはsafeフィルタでエスケープしないようにする。
templates/includes/form.htmlのfield.help_textに対して|safeフィルタを追加する。
{% if field.help_text %}
<small class="form-text text-muted">
{{ field.help_text|safe }}
</small>
{% endif %}

ビューの処理
ビューの処理をaccounts/views.pyに記述する。
from django.contrib.auth import login from django.contrib.auth.forms import UserCreationForm from django.shortcuts import render, redirect def signup(request): if request.method == 'POST': form = UserCreationForm(request.POST) if form.is_valid(): user = form.save() login(request, user) return redirect('home') else: form = UserCreationForm() return render(request, 'signup.html', {'form': form})
基本的なフォームの処理と流れは同じで、フォームのデータに問題がなければuser = form.save()でユーザーインスタンスを作成する。
そしてlogin(request, user)メソッドでログイン処理を行い、homeページにリダイレクトする。
実際にデータを送信してみるが、最初は登録済みユーザ名とパスワードを未入力にして試してみる。

図のようなバリデーションメッセージが表示される。
次に適切なユーザ名とパスワードを入力して送信してみる。

homeページにリダイレクトされ登録は完了したようにみえる。
だが、これでは実際にログインできているのかわからないのでログイン中のユーザ名を表示するようにtemplates/base.htmlを変更する。
{% block body %}
<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'home' %}">Django Boards</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#mainMenu" aria-controls="mainMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainMenu">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="#">{{ user.username }}</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
<ol class="breadcrumb my-4">
{% block breadcrumb %}
{% endblock %}
</ol>
{% block content %}
{% endblock %}
</div>
{% endblock body %}
{{ user.username }}がユーザ名でナビゲーションバー(右上)にログイン中のユーザ名が表示されるようになる。

なお、DjangoではCookieのsessionidキーでセッションIDを保持しているので、ログイン後にsessionidが追加されているのがわかる。

データベースの確認
サインアップでユーザーが登録されているかデータベースを確認してみる。
ユーザー情報はauth_userテーブルに登録されるがスキーマ情報は以下のようになっている(見やすいようにフォーマット)。
sqlite> .schema auth_user CREATE TABLE IF NOT EXISTS "auth_user"( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "password" varchar(128) NOT NULL, "last_login" datetime NULL, "is_superuser" bool NOT NULL, "username" varchar(150) NOT NULL UNIQUE, "first_name" varchar(30) NOT NULL, "email" varchar(254) NOT NULL, "is_staff" bool NOT NULL, "is_active" bool NOT NULL, "date_joined" datetime NOT NULL, "last_name" varchar(150) NOT NULL ) ;
登録されているデータを見てみる(見やすいようにフォーマット)。
adminユーザはcreatesuperuserコマンドで既に作成しているユーザでis_superuserとis_staffが1になっているが、サインアップ画面で作成したtestuserの値は0になっている。
passwordカラムがパスワード情報でドキュメントによると<algorithm>$<iterations>$<salt>$<hash>というフォーマットで保存されている。
sqlite> select * from auth_user;
1|
pbkdf2_sha256$100000$yFAJbAMkVsTF$q9c97Ad5Zm5tKWdurCEg3o2ddKj7SeP7YGdX5lRC12c=|
2018-03-07 15:29:17.736686|
1|
admin|
|
admin@example.com|
1|
1|
2018-03-07 15:17:27.111360|
2|
pbkdf2_sha256$100000$ffdHNEg7WdJo$QEU0OkNts7VC8+6AwMFcKKZ9j10YXSsi9qn/d3kkl4o=|
2018-03-21 04:58:45.723989|
0|
testuser|
|
|
0|
1|
2018-03-21 04:58:45.568002|
まとめ
- ユーザー認証用のアプリは別アプリとして作成
UserCreationFormというユーザー登録向けのフォームがある|safeフィルタでエスケープしないようにできるlogin(request, user)でログイン処理- Cookieの
sessionidキーでセッションIDを保持