Djangoメモ(19) : ユーザー認証を実装する〜サインアップ(ユーザー登録)
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を保持