Agile育成ブログ
未来を変える喜びを
Django

Djangoカスタムユーザー会員サイトを作る(django-allauth)


Warning: count(): Parameter must be an array or an object that implements Countable in /home/xs638785/agile-software.site/public_html/wp-content/plugins/rich-table-of-content/functions.php on line 490

認証の仕組み

セッション認証(Cookie認証)を利用したログインは

  1. ユーザー名・パスワードと事前に登録されたユーザーテーブルのユーザー名・パスワードで本人確認を行う
  2. 本人確認後、ログイン情報をセッションに保存しキーとなるセッションIDを発行する
  3. このセッションIDはレスポンスでブラウザに返され、ブラウザ側の「Cookie」に保存されます。
  4. 再びログインする際にセッションIDが送信されることになるため本人確認を行う必要がなくなります。

GitHubとの連携

https://www.agile-software.site/2021/04/24/vs-code%e3%81%ae%e8%a8%ad%e5%ae%9a/

仮想環境作成・実行

python3 -m venv myvenv
source myvenv/bin/activate

requirements.txt作成

Django~=3.1.4
Pillow~=8.1.0
django-widget-tweaks~=1.4.8
stripe~=2.27.0
Pillow~=8.1.0

パッケージのインストール

(myvenv) ~$ pip3 install -r requirements.txt

すでにあるプロジェクトをコピーしている場合は以下の工程を省略できます。

プロジェクト作成

(myvenv) ~$ django-admin startproject mysite .

アプリケーション作成

(myvenv) ~$ python3 manage.py startapp app
(myvenv) ~$ python3 manage.py startapp accounts

settings.py

  • django.contrib.admin:管理(admin)サイト
  • django.contrib.auth:認証システム
  • django.contrib.contenttypes:コンテンツタイプフレームワーク
  • django.contrib.sessions:セッションフレームワーク
  • django.contrib.messages:メッセージフレームワーク
  • django.contrib.staticfiles:静的ファイルの管理フレームワーク
  • widget_tweaks:
  • app:作成したappアプリケーションを登録
  • accounts:作成したaccountsアプリケーションを登録
  • django.contrib.sites:下記解説あり
  • allauth:
  • allauth.account:
  • allauth.socialaccount:ソーシャルログイン機能を実装
django.contrib.sites
  • Djangoに付属するsitesフレームワークを導入すると、1つのウェブサイトに1つのSiteデータが割り当てられます。これとOneToOneで紐づくSiteDetailのようなモデルを作ることで、そのウェブサイト全体の設定を表すモデルが作れるらしいです。
allauth
  • django-allauthはsitesフレームワークの利用が必要
  • SITE_ID = 1 :sitesフレームワーク用のサイトID
  • LOGIN_REDIRECT_URL = ‘/’ :ログイン時のリダイレクト先
  • ACCOUNT_LOGOUT_REDIRECT_URL = ‘/’ :ログアウト時のリダイレクト先
  • ACCOUNT_EMAIL_VERIFICATION = ‘none’ :ユーザ登録時に確認メールを送信するか(none=送信しない, mandatory=送信する)
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'widget_tweaks', # 追加
    'app', # 追加
    'widget_tweaks', # 追加
    'app', # 追加
    'accounts', # 追加
    'django.contrib.sites', # 追加
    'allauth', # 追加
    'allauth.account', # 追加
    'allauth.socialaccount' # 追加
]

LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'


SITE_ID = 1
LOGIN_REDIRECT_URL = '/'
ACCOUNT_LOGOUT_REDIRECT_URL = '/'
ACCOUNT_EMAIL_VERIFICATION = 'none'

プロジェクトURL

アプリケーションのURLであるapp.urlsとaccounts.urlsを追加します。

urlpatternsにpath(‘accounts/’,include(‘django.contrib.auth.urls’))を追加すると認証周りのすべてのURLパターンが設定されます。

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('app.urls')),
    path('accounts/', include('accounts.urls')),
]

アプリケーションURL(app)

from django.urls import path
from app import views

urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
]

views(app)

from django.views.generic import TemplateView


class IndexView(TemplateView):
    template_name = "app/index.html"

base.html(app)

{% load static %}

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
    <title>カスタム認証チュートリアル</title>
</head>

<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <ul class="navbar-nav ml-auto">
                <li class="nav-item">
                    <a class="nav-link" href="/">ホーム</a>
                </li>
            </ul>
        </div>
    </nav>

    <main>
        <div class="container">
            {% block content %}
            {% endblock %}
        </div>
    </main>

    <footer class="py-2 bg-dark">
        <p class="m-0 text-center text-white">Copyright © Django Startup 2020</p>
    </footer>

    {% block extra_js %}
    {% endblock %}

</body>

</html>

index.html(app)

{% extends "app/base.html" %}

{% block content %}

<div class="my-5">
    <h1>Hello World!!</h1>
</div>

{% endblock %}

CSS(app)

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    background: #F1F1F1;
    display: flex;
    flex-flow: column;
    min-height: 100vh;
}

main {
    flex: 1;
}

アプリケーションURL(accounts)

from django.urls import path
from accounts import views

urlpatterns = [
    path('profile/', views.ProfileView.as_view(), name='profile'),
]

流れ

  1. 127.0.0.1:8000をrequest
  2. urls.pyがProfileviewを呼び出す
  3. Profileviewの中でreturn renderが実行され、profile.htmlが画面に表示される

データベースの削除

view(accounts)

ログイン・ログアウトのViewはDjango側がdjango.contrib.authに用意してくれているので、それらをimportして使います。

モデルで作成したCustomUserクラスからログイン中のユーザー情報を取得

user_data = CustomUser.objects.get(id=request.user.id)
from django.views import View
from accounts.models import CustomUser
from django.shortcuts import render, redirect


class ProfileView(View):
    def get(self, request, *args, **kwargs):
        user_data = CustomUser.objects.get(id=request.user.id)

        return render(request, 'accounts/profile.html', {
            'user_data': user_data,
        })

profile.html(accounts)

{% extends "app/base.html" %}

{% block content %}

<div class="card card-profile my-5 mx-auto">
    <div class="card-body">
        <h5 class="card-title text-center">プロフィール</h5>
        <table class="profile_table mb-4">
            <tbody>
                <tr>
                    <th class="header">名前</th>
                    <td class="data">xxx</td>
                </tr>
                <tr>
                    <th class="header">メールアドレス</th>
                    <td class="data">xxx</td>
                </tr>
                <tr>
                    <th class="header">所属</th>
                    <td class="data">xxx</td>
                </tr>
                <tr>
                    <th class="header">入会日</th>
                    <td class="data">xxx</td>
                </tr>
            </tbody>
        </table>
        <div class="button mx-auto">
            <a class="btn btn-lg btn-warning btn-block" href="">編集する</a>
        </div>
    </div>
</div>

{% endblock %}

カスタムユーザー作成

django-allauth

django-allauthは以下の機能を備えています。

  • ユーザログオン機能
  • ユーザログアウト機能
  • パスワード変更
  • パスワード再設定
  • ユーザ登録

Djangoで会員登録機能を使う場合はUserモデルを使います。
しかし、実はUserモデルはprojectにつき1つしか使えません。

settings.pyで設定するAUTH_USER_MODELの部分を以下の例ではaccountsアプリケーションに対して設定しています。

機能URLパターンビュー/フォーム
ログイン /accounts/login/ LoginView
ログアウト /accounts/ logout/ LogoutView
パスワード変更 /accounts/ password_change/ PasswordChangeView
パスワード変更完了 /accounts/ password_change/done PasswordChangeDoneView
パスワード再設定メール送信 /accounts/ password_reset/ PasswordResetView
パスワード再設定メール送信完了 /accounts/ password_reset/done/ PasswordResetDoneView
パスワード再設定 /accounts/ reset/<uidb64>/<token>/ PasswordResetConfirmView
パスワード再設定完了 /accounts/ reset/done/ PasswordResetCompleteView
AUTH_USER_MODEL = 'accounts.CustomUser'

DjangoのデフォルトのUserモデルでは、ユーザー名を表すusernameというフィールドがあります。しかし、Webアプリによってはユーザー名としてメールアドレスを利用することも多いため、今回はusernameフィールドをなくし、emailフィールドをメインに扱うモデルを作成します

2つの方法があります。

  • django.contrib.auth.base_user.AbstractBaseUserを継承する
  • django.contrib.auth.models.AbstractUserを継承する

AbstractBaseUserを継承する

AbstractUserはデフォルトのUserモデルが持っている機能を全て持っており、デフォルトUserに少し手を入れたい場合に向いています。よくやるのは、emailフィールドを必須にしてみたり、電話番号や住所といったフィールドの追加です。

AbstractUserを継承する

AbstractBaseUserは最低限な機能だけを持っています。AbstractUserが持っているフィールドの幾つかを消したり、大きな変更が必要な場合は、こちらの継承を使います。

UserManager

create_userはユーザーの新規作成時に呼び出されるメソッド。
create_superuserは管理者用のユーザーを作成するときに使われるメソッド。

class UserManager(UserManager):
    def _create_user(self, email, password, **extra_fields):
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(email, password, **extra_fields)

CustomUser

AbstractBaseUserクラスを継承し、新たにCustomUserクラスを作成します。

class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField('メールアドレス', unique=True)
    first_name = models.CharField(('姓'), max_length=30)
    last_name = models.CharField(('名'), max_length=30)
    department = models.CharField(('所属'), max_length=30, blank=True)
    created = models.DateTimeField(('入会日'), default=timezone.now)

    is_staff = models.BooleanField(
        ('staff status'),
        default=False,
        help_text=('Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        ('active'),
        default=True,
        help_text=(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )

    objects = UserManager()

    USERNAME_FIELD = 'email'
    EMAIL_FIELD = 'email'
    REQUIRED_FIELDS = []

    class Meta:
        verbose_name = ('user')
        verbose_name_plural = ('users')

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)
from django.db import models
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import UserManager, PermissionsMixin
from django.utils import timezone

管理画面

from django.contrib import admin

from .models import CustomUser

admin.site.register(CustomUser)
AUTH_USER_MODEL = 'accounts.CustomUser'
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False

サインアップ

accountsからviewsをインポートします。
urlpatternsにおいてユーザーからsignupのリクエストが来た時SignupViewを表示させます。

path('signup/', views.SignupView.as_view(), name='account_signup'),
from django.urls import path
from accounts import views

urlpatterns = [
    path('signup/', views.SignupView.as_view(), name='account_signup'),
    path('login/', views.LoginView.as_view(), name='account_login'),
    path('logout/', views.LogoutView.as_view(), name='account_logout'),
    path('profile/', views.ProfileView.as_view(), name='profile'),
    path('profile/edit/', views.ProfileEditView.as_view(), name='profile_edit'),
]

サインアップのフォームを作成

メールアドレスとパスワードは、allauthですでに設定してあるので必要ありません。

ここでは名前のみ追加しておきます。

サインアップボタンが押された時にsave関数が実行されるようにします。self.cleaned_dataでフォームに記載された内容を取得し、save関数でデータベースに保存します。

    def save(self, request):
        user = super(SignupUserForm, self).save(request)
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.save()
        return user
from django import forms
from allauth.account.forms import SignupForm # 追加


class SignupUserForm(SignupForm):
    first_name = forms.CharField(max_length=30, label='姓')
    last_name = forms.CharField(max_length=30, label='名')

    def save(self, request):
        user = super(SignupUserForm, self).save(request)
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.save()
        return user

views

forms.pyで作成したSignupUserFormをfrom_class変数に設定することでオリジナルのフォームを使用することができます。テンプレートはaccounts/signup.htmlです。

from django.views import View
from accounts.models import CustomUser
from accounts.forms import ProfileForm, SignupUserForm # 追加
from django.shortcuts import render, redirect
from allauth.account import views


class SignupView(views.SignupView):
    template_name = 'accounts/signup.html'
    form_class = SignupUserForm

テンプレート

signup.html

base.htmlの動的な部分にsignup.htmlを使用します。

{% extends "app/base.html" %}
{% load widget_tweaks %}

{% block content %}

<div class="card card-auth my-5 mx-auto">
    <div class="card-body">
        <h5 class="card-title text-center">サインアップ</h5>
        <form method="post" class="form-auth">
            {% csrf_token %}
            <div class="form-label-group form_wrap form_wrap-2">
                {% render_field form.first_name class="form-control" placeholder="姓" %}
                {% render_field form.last_name class="form-control" placeholder="名" %}
            </div>
            <div class="form-label-group">
                {% render_field form.email class="form-control" placeholder="メールアドレス" %}
            </div>
            <div class="form-label-group">
                {% render_field form.password1 class="form-control" placeholder="パスワード" %}
            </div>
            <div class="form-label-group">
                {% render_field form.password2 class="form-control" placeholder="パスワード(確認)" %}
            </div>
            <div class="button mx-auto">
                <button class="btn btn-lg btn-primary btn-block mx-auto" type="submit">サインアップ</button>
            </div>
        </form>
    </div>
</div>

{% endblock %}

base.html

ナビゲーションにサインアップを追加し、ログアウト時に表示するようにします。

You cannot copy content of this page