Djangoのカスタムユーザー認証機能を使用する！！

## はじめに
こんにちは。@toamoku-20220418と申します。現在Djangoを使用したアプリケーションの作成を目標に、勉強を進めております。
　今回はDjangoのユーザー認証機能をカスタマイズした、簡単なサンプルアプリを作成します。併せて管理者画面も若干カスタムしてみました。
　ぜひ最後までご覧いただき、皆さんの参考になれば幸いです。
## 前提条件
　PCにはpythonがインストールされていることと、GitHubは使用しないことを前提にお話を進めてまいります。SECRET_KEYなどもそのままで進めてまいりますので、GitHubなどでPublicへ公開する予定の方は、私の下記の記事をご覧に頂けると参考になると思いますので、併せてご覧ください。また使用PCはmacです。ご了承ください。

 https://qiita.com/toamoku-20220418/items/9419ba85f56c119603ba

 ## 今回の目標
　Djangoのユーザー認証機能をカスタマイズして、管理者画面も併せてカスタマイズする方法を理解する。

## 事前準備
　プロジェクトディレクトリを作成や仮想環境などの設定を行います。
　最初にプロジェクトディレクトリを作成し、プロジェクトディレクトリへ移動します。
```bash
$ mkdir myproject

$ cd myproject
```
　次に仮想環境を作成します。
```bash
virturalenvの場合
$ virtualenv <仮想環境名>

venvの場合
$ python3 -m venv <仮想環境名>
```
　仮想環境の有効化
```bash
$ source <仮想環境名>/bin/activate
```
　続いてDjangoをインストールします。
```bash
$ pip install django
```
## Djangoプロジェクトとアプリの作成
　カンマはあってもなくてもオッケーです。カンマがない場合はディレクトリが２個作成され、２個目にファイルが作成されています。
```bash
Djangoプロジェクトの作成
ディレクトリを作成しているのでこれでOKです
$ django-admin startproject myproject . 

Djangoアプリの作成
$ python manage.py startapp myapp
```
## ホーム画面の作成
　アプリの最初の画面であるホーム画面を作成します。
　最初にsettings.pyにmyappを追加します。
 ```python:settings.py
 # myproject/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp' # ここを追加
]
```
　続いてmyprojectのurls.pyを編集します。
```python:urls.py
# myproject/urls.py

from django.contrib import admin
from django.urls import path, include # includeを追加

urlpatterns = [
    path('admin/', admin.site.urls),
    # ここを追加。admin以外のurlは全てmyappのurls.pyを参照する
    path('', include('myapp.urls')),
]
```
　続いてmyappにurls.pyを作成し、編集します。
```python:urls.py
from django.urls import path
from . import views

# ホーム画面用のルーティング
urlpatterns = [
    path('', views.home, name='home'),
]
```
ホーム画面を表示するビューを作成します。
```python:views.py
# myapp/views.py

from django.shortcuts import render

def home(request):
    return render(request, 'home.html')
```
　templatesディレクトリを作成して、その中にhome.htmlを作成します。
```html:home.html
<!-- templates/home.html -->
<h1>home</h1>
```
　settings.pyにtemplatesを追加します。
```python:settings.py
# myproject/settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],# ここに　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',
            ],
        },
    },
]
```
　動作確認
 ここでいったん動作確認をしてみましょう。
 localhost:8000にアクセスして「home」と表示されればOKです。
## 言語と時間の編集
　settings.pyの言語と時間の編集を行います。
```python:settings.py
# myproject/settings.py

LANGUAGE_CODE = 'ja' # 変更

TIME_ZONE = 'Asia/Tokyo' # 変更

USE_TZ = True
```

## ユーザーモデルの作成
　ここからユーザーモデルを作成します。
CustomUserManagerクラスを作成し、一般ユーザーと管理者ユーザーのメソッドを定義します。
```python:models.py
# myapp/models.py

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager,PermissionsMixin
from datetime import datetime

class CustomUserManager(BaseUserManager):
    def create_user(self, email, username, password=None):
        if not email: # emailが未入力
            raise ValueError('メールアドレスを入力してください')
        if not username: # usernameが未入力
            raise ValueError('ユーザー名を入力してください')
        
        email = self.normalize_email(email)
        user = self.model(email=email, username=username)
        user.set_password(password)
        user.save(using=self._db) # DBに保存
        return user
    
    def create_superuser(self, email, username, password=None):
        user = self.create_user(email, username, password)
        user.is_staff = True
        user.is_superuser = True
        user.save(using=self._db)
        return user
```
続いてCustomUserクラスを作成します。
```python:models.py
# myapp/models.py

class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(max_length=255, unique=True, default='email@example.com')
    username = models.CharField(max_length=50)
    date_joined = models.DateTimeField(default=datetime.now())
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    
    objects = CustomUserManager()
    
    USERNAME_FIELD = 'email' # 一意の識別子
    REQUIRED_FIELDS = ['username'] # superuser作成に必要な項目
    
    
    def __str__(self):
        return self.email
```
次にsettings.pyに上記のクラスを使用することを記述します。
```python:settings.py
# myproject/settings.py

AUTH_USER_MODEL = 'myapp.CustomUser'
```
ここまで出来たら一旦マイグレーションを行います。
```bash
作成したモデルをマイグレーションする
$ python manage.py makemigrations myapp

データベースに反映
$ python manage.py migrate
```
## ユーザー新規登録の作成
最初にユーザー登録用のフォームを作成します。
```python:forms.py
# myapp/forms.py

from django import forms
from .models import CustomUser

class UserRegistrationForm(forms.ModelForm):
    username = forms.CharField(label='ユーザー名')
    email = forms.EmailField(label='メールアドレス')
    password = forms.CharField(label='パスワード', widget=forms.PasswordInput)
    confirm_password = forms.CharField(label='確認用パスワード', widget=forms.PasswordInput)
    
    class Meta:
        model = CustomUser # 使用するモデル
        fields = ('email', 'username')
        
    def clean_confirm_password(self):
        cd = self.cleaned_data
        if cd['password'] != cd['confirm_password']:
            raise forms.ValidationError('パスワードが一致しません')
        return cd['confirm_password']
```
　続いてユーザー登録用のビューを作成します。
```python:views.py
# myapp/views.py

from django.shortcuts import render, redirect
# ここから下を追加
from django.contrib import messages
from .forms import UserRegistrationForm

# homeビューの下に追加
def register(request):
    if request.method == 'POST':
        form = UserRegistrationForm(request.POST)
        if form.is_valid():
            user = form.save(commit=False)
            user.set_password(form.cleaned_data['password'])
            user.save()
            login(request, user)
            messages.success(request, '新規登録が完了しました')
            return redirect('home') # ホーム画面へリダイレクト
        else:
            messages.error(request, '新規登録に失敗しました。再度登録をお願いします')
    else:
        form = UserRegistrationForm()
    return render(request, 'register.html', {'form': form})
```
urls.pyに追加します。
```python:urls.py
# myapp/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='home'),
	path('register/', views.register, name='register'), # ここを追加
]
```
ここでregister.html作成の前にbase.htmlを作成します。
```html:base.html
<!-- templates/base.html -->

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    <!-- メッセージを表示する -->
    {% if messages %}
    <ul>
        {% for message in messages %}
        <li class="{{ message.tags }}">{{ message }}</li>
        {% endfor %}
    </ul>    
    {% endif %}
    {% block content %}
    {% endblock %}
</body>
</html>
```
続いてregister.htmlを作成します。
```html:register.html
<!-- templates/register.html -->

{% extends "base.html" %}
{% block title %}新規登録{% endblock title %}
{% block content %}
<h1>ユーザー登録</h1>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">登録</button>
</form>
<p><a href="{% url 'home' %}">ホームへ</a></p>
{% endblock %}
```
base.htmlを使用したhome.htmlに修正します。
```html:home.html
<!-- templates/home.html -->

{% extends "base.html" %}
{% block title %}ホーム{% endblock title %}
{% block content %}
<h1>ホーム</h1>
<a href="{% url 'register' %}">新規登録</a>
{% endblock %}
```
ここで動作確認を行います。
1. localhost:8000にアクセス
2. 新規登録へ遷移
3. 新規ユーザー登録を実行
4. ホーム画面へリダイレクトされて「新規登録が完了しました」と表示されれば、オッケー
5. 新規登録画面からホーム画面に戻れるかどうかも確認
## ログイン機能の実装
　最初にログイン用のフォームを作成します。
```python:forms.py
myapp/forms.py

class UserLoginForm(forms.Model):
		email = forms.EmailField(label='メールアドレス', unique=True)
		password = forms.CharField(label='パスワード', widjet=PasswordInput)
```
続いてログイン用のビューを作成します。
```python:views.py
# myapp/views.py
from .forms import UserRegistrationForm, UserLoginForm # UserLoginFormを追加

def user_login(request)
		if request.method == 'POST': # POSTメソッドの場合
			 form = UserLoginForm(request.method='POST')
			 if form.is_valid(): # formのバリデーション
				  email = forms.cleaned_data['email']
				  password = forms.cleaned.data['password']
				  user = authenticate(request, email=email, password=password) # userの存在確認
				  if user is not None: # userが存在する場合
		            login(request, user) # 登録時にログインさせる
		            messages.success(request, 'ログインしました')
		            return redirect('home') # 仮でhomeへリダイレクト
		        else:
		            messages.error(request, '入力に誤りがあります')
		    else:
		        messages.error(request, 'ログイン出来ませんでした。')
		else:
		    form = UserLoginForm()
		return render(request, 'login.html', {'form': form})
```
ログイン用のhtmlファイルを作成します。
```html:login.html
<!-- templates/login.html -->

{% extends "base.html" %}
{% block title %}LOGIN{% endblock %}

{% block content %}
<h1>ログイン</h1>
<form method='POST'>
    {% csrf_token %}
    {{ form.as_p }}
    <button type='submit'>ログイン</button>
</form>
<p><a href="{% url 'home' %}">ホームへ</a></p>
{% endblock %}
```
home.htmlにログインを追加
```html:home.html
<!-- templates/home.html -->

{% extends "base.html" %}
{% block title %}ホーム{% endblock title %}
{% block content %}
<h1>ホーム</h1>
<a href="{% url 'register' %}">新規登録</a>
<a href="{% url 'login' %}">ログイン</a> # ここを追加
{% endblock %}
```
そしてurls.pyにログインを追加します。
```python:urls.py
# myapp/urls.py

urlpatterns = [
    path('', views.home, name='home'),
	path('register/', views.register, name='register'),
    path('login/', views.user_login, name='login'),  # ここを追加
]
```
ここでいったんログインしてみましょう。ホーム画面からログインを実行し、ホーム画面へ戻って「ログインしました」とメッセージが表示されれば成功です。
## ログアウト機能の実装
続いてログアウト機能を実装します。最初にログアウト用のビューを作成します。
```python:views.py
# myapp/views.py
from django.contrib.auth.decorators import login_required # ここを追加

@login_required # ログインしている場合に実行
def user_logout(request):
    logout(request)
    messages.success(request,'ログアウトが完了しました')
    return redirect('home')
```
続いてurls.pyにログアウトを追加します。
```python:urls.py
# myapp/urls.py

urlpatterns = [
    path('', views.home, name='home'),
	path('register/', views.register, name='register'),
    path('login/', views.user_login, name='login'),
    path('logout/', views.user_logout, name='logout'), # ここを追加
]
```
動作確認をします。ログインが完了後、urlに「localhost:8000/logout/」を入力して、ホーム画面に「ログアウトが完了しました」と表示されればオッケーです。
## ユーザー詳細画面の実装
最初にuser_detail.htmlを作成します。
```html:user_detail.html
<!-- templates/user_detail.html -->
{% extends "base.html" %}

{% block title %}ユーザー詳細{% endblock title %}

{% block content %}
<h1>ユーザー詳細</h1>
<p>ユーザー名：{{ request.user.username }}</p>
<p>メールアドレス：{{ request.user.email }}</p>
<nav>
    <a href="{% url 'logout' %}">ログアウト</a>
</nav>
{% endblock %}
```
続いてビューを作成します。
```python:views.py
# myapp/views.py

@login_required
def user_detail(request):
    return render(request, 'user_detail.html')
```
urls.pyにも追加します。
```python:urls.py
# myapp/urls.py

urlpatterns = [
    path('', views.home, name='home'),
	path('register/', views.register, name='register'),
    path('login/', views.user_login, name='login'),
    path('logout/', views.user_logout, name='logout'),
    path('user/', views.user_detail, name='detail'), # ここを追加
]
```
次に新規登録とログイン後のリダイレクト先をhomeからdetailに変更します。
```python:views.py
# myapp/views.py

def register(request):
		# 下記を修正
		return redirect('detail') 

def user_login(request):
		# 下記を修正
		return redirect('detail')
```
これで新規登録とログイン時に遷移先をホーム画面から詳細画面に変更しました。
ここで動作確認をしてみましょう。新規登録とログイン時に詳細画面へ遷移されればオッケーです。
## ユーザー情報編集機能の実装
最初にユーザー編集用のフォームを作成します。
```python:forms.py
myapp/forms.py

class UserEditForm(forms.ModelForm):
    new_password = forms.CharField(label='新パスワード', widget=forms.PasswordInput, required=False)
    confirm_password = forms.CharField(label='パスワード確認用', widget=forms.PasswordInput, required=False)
    
    class Meta:
        model = CustomUser
        fields = ('email', 'username')
    
    def clean(self):
        cleaned_data = super().clean()
        new_password = cleaned_data.get('new_password')
        confirm_password = cleaned_data.get('confirm_password')
        # 入力したパスワードが一致しているか確認
        if new_password and new_password != confirm_password:
            raise forms.ValidationError('パスワードが一致しません')
    
        return cleaned_data
```
続いて編集用のビューを作成します。
```python:views.py
# myapp/views.py

# UserEditFormを追加
from .forms import UserRegistrationForm, UserLoginForm, UserEditForm

# 下記を追加
@login_required
def user_edit(request):
    if request.method == 'POST':
        form = UserEditForm(request.POST, instance=request.user)
        if form.is_valid():
            form.save()
            messages.success(request, 'ユーザー情報を更新しました')
            return redirect('detail')
        else:
            messages.error(request, '失敗したので再度更新してください')
    else:
        form = UserEditForm(instance=request.user)
    return render(request, 'user_edit.html', {'form': form})
```
次にuser_edit.htmlを作成します。
```html:user_edit.html
<!-- templates/user_edit.html -->
{% extends "base.html" %}

{% block title %}編集{% endblock %}

{% block content %}
<h1>ユーザー情報編集</h1>
<form method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <button type='submit'>更新</button>
</form>
<p><a href="{% url 'detail' %}">戻る</a></p>
{% endblock %}
```
urls.pyにも追加します。
```python:urls.py
# myapp/urls.py

urlpatterns = [
    path('', views.home, name='home'),
	path('register/', views.register, name='register'),
    path('login/', views.user_login, name='login'),
    path('logout/', views.user_logout, name='logout'),
    path('user/', views.user_detail, name='detail'),
    path('user/edit/', views.user_edit, name='edit'), # ここを追加
]
```
詳細画面から編集画面への遷移を追加します。
```html:user_detail.html
<!-- templates/user_detail.html -->
{% extends "base.html" %}

{% block title %}detail{% endblock title %}

{% block content %}
<h1>ユーザー詳細</h1>
<p>ユーザー名：{{ request.user.username }}</p>
<p>メールアドレス：{{ request.user.email }}</p>
<nav>
    <a href="{% url 'edit' %}">編集</a> # ここを追加
    <a href="{% url 'logout' %}">ログアウト</a>
</nav
{% endblock %}
```
ここで動作確認をしてみましょう。わざと入力ミスなどもしてみて挙動を確認してみてください。
## 管理者サイトのカスタマイズ
管理者権限のユーザー登録フォームを作成します。
```python:forms.py
# myapp/forms.py

# 下記を追加
# 管理者権限でのユーザー作成用フォーム
class UserCreationForm(forms.ModelForm):
    username = forms.CharField(label='ユーザー名')
    email = forms.EmailField(label='メールアドレス')
    password1 = forms.CharField(label='パスワード', widget=forms.PasswordInput)
    password2 = forms.CharField(label='パスワード確認用', widget=forms.PasswordInput)

    class Meta:
        model = CustomUser
        fields = ('email', 'username')

    def clean_password2(self):
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise forms.ValidationError("パスワードが一致しません")
        return password2

    def save(self, commit=True):
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user
```
続いてadmin.pyを編集し、管理者サイトの設定を行います。
```python:admin.py
# myapp/admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import CustomUser
from .forms import UserCreationForm

class UserAdmin(BaseUserAdmin):
    add_form = UserCreationForm # ユーザー追加時に使用するフォームの設定

    # リスト画面の表示項目とフィルター機能をもつ項目
    list_display = ('email', 'username', 'date_joined', 'is_staff', 'is_superuser')
    list_filter = ('email', 'username','is_staff', 'is_superuser')

    # ユーザー選択時に表示されるフィールド
    fieldsets = (
        ('ユーザー情報', {'fields': ('email', 'username', 'date_joined')}),
        ('権限', {'fields': ('is_active', 'is_staff', 'is_superuser')}),
    )

    # 管理者画面でのユーザー作成時のフィールドを設定
    add_fieldsets = (
        ('ユーザー作成', {
            'classes': ('wide',),
            'fields': ('email', 'username', 'password1', 'password2'),
        }),
    )
    search_fields = ('email', 'username') # 検索するフィールド
    ordering = ('date_joined',) # リストの並び順
    filter_horizontal = ()

admin.site.register(CustomUser, UserAdmin)
```
settings.pyの言語設定などを変更します。
```python:settings.py
# myproject/settings.py

# 下記の２項目を変更
LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'
```
これで管理者サイトが日本語表記になります。
最後にスーパーユーザーを作成しましょう。myprojectディレクトリで下記のコマンドを実行します。
```bash
$ python manage.py createsuperuser
```
必要な入力が完了したら、スーパーユーザーの作成は終了です。
そのままlocalhost:8000/adminから作成したスーパーユーザーでログインし、管理者サイトに移動しましょう。
カスタマイズされた管理者サイトになっていればオッケーです。
Custom Userを選択してみください。ユーザーリストが表示されます。
表示項目が　list_displayと同じになっていればオッケーです。また右サイドにlist_filterと同じ項目でフィルターを出来るようになっています。
## 最後に
　いかがでしたでしょうか。今回はDjangoのAbstractBaseUserを使用して、カスタムユーザーモデルを作成し、認証機能を作成してみました。
 今回は非常に勉強になりました。そして認証機能の基本をしっかりと学ぶことが出来たと思います。また管理者サイトもカスタマイズすることができるようになりました。コードについて、何かございましたらご連絡いただけると幸いです。今後も継続的に記事を投稿していきますので、よろしくお願いします。