Python(Django)でSNSアプリケーションの開発Part2~ユーザー認証機能~

[前回の記事](https://qiita.com/Taku_nakahara/items/50ee5d98a7e86fcd8475)では初期設定とサイトの全体像について紹介しました。
今回はユーザー認証機能について解説します。

<h2>ユーザー認証機能とは</h2>
ユーザー認証機能とは「サインアップ」「ログイン」「ログアウト」「パスワードリセット」などユーザーに関する機能です。
システムに登録しているユーザーを識別したり、新たに登録するといった役割があります。

<h2>Djangoでユーザー認証機能を作成する</h2>
<h5>作成方法概要</h5>
[前回の記事]から勧められた方は以下のようなディレクトリ構成になっていると思います。

```Python:ディレクトリ
app_sns/
        ├プロジェクト
        ├ manage.py
        └ accounts(app_name)/
            ├ __init__.py
            ├ apps.py
            ├ models.py
            ├ tests.py
            ├ views.py
            └ urls.py #追加
        └ timelines(app_name)/
            ├ __init__.py
            ├ apps.py
            ├ models.py
            ├ tests.py
            ├ views.py
            └ urls.py #追加
```
ここではaccountsの方を利用してユーザー認証機能を実装しましょう。
<h5>コード全文</h5>
ユーザー作成にはカスタムユーザーモデルという機能を利用しました。
簡単にユーザー認証機能が実装できます。(Django公式でも利用が推奨されている機能)

```Python:models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.urls import reverse
# Create your models here.

class User(AbstractUser):
    pass
```
forms.pyではサインアップに必要な項目を記載しています。
①ユーザー名
②emailアドレス
③パスワード
④確認用

これらをサインアップフォームに入力し送信するという流れになります。

```Python:forms.py(accounts内に作成)
from django.contrib.auth.forms import UserCreationForm
from .models import User

class UserCreationForm(UserCreationForm):

    class Meta:
        model = User
        fields = ('username', 'email', 'password1', 'password2')
```

```Python:urls.py(プロジェクト内)
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('accounts.urls')),
    path('password_reset/', auth_views.PasswordResetView.as_view(template_name = 'password_reset.html'), name = 'password_reset'),
    path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(template_name = 'password_reset_sent.html'), name = 'password_reset_done'),
    path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(template_name = 'password_reset_form.html'), name = 'password_reset_confirm'),
    path('reset/done/', auth_views.PasswordResetCompleteView.as_view(template_name = 'password_reset_done.html'), name = 'password_reset_complete'),
]
```

```Python:urls.py(アプリケーション内)
from django.urls import path
from . import views
from django.contrib.auth import views as auth_views

app_name = 'accounts'

urlpatterns = [
    path('signup/', views.SignupView.as_view(), name = 'signup'),
    path('signup_done/', views.SignupDoneView.as_view(), name = 'signup_done'),
    path('login/', auth_views.LoginView.as_view(template_name = 'login.html'), name = 'login'),
    path('logout/', auth_views.LogoutView.as_view(template_name = 'logout.html'), name = 'logout'),
]
```
views.pyではサインアップについての機能しか書かれていないのですが、そのほかの機能はカスタムユーザーモデルを利用するとurls.pyで簡単に記載できます。
またパスワードリセットのみプロジェクト内のurls.pyに記載することに注意してください。

```Python:views.py
from django.shortcuts import render, get_object_or_404
from django.views.generic import CreateView, TemplateView
from .forms import UserCreationForm
from django.urls import reverse_lazy
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from .models import User
from timelines.models import Post
# Create your views here.

class SignupView(CreateView):
    form_class = UserCreationForm
    template_name = 'signup.html'
#サインアップ処理に成功したらサインアップ成功ページに遷移
    success_url = reverse_lazy('accounts:signup_done')
#forms.pyで設定した項目に入力し、ちゃんと入力できているか判定
    def form_valid(self, form):
        user = form.save()
        self.object = user
        return super().form_valid(form)

class SignupDoneView(TemplateView):
    template_name = 'signup_done.html'
```

```Python:admin.py
from django.contrib import admin
from .models import User
# Register your models here.

class UserAdmin(admin.ModelAdmin):
    list_display = ('id', 'username', 'email')
    list_display_links = ('id', 'username')

admin.site.register(User, UserAdmin)
```

<h5>テンプレートについて</h5>
HTMLのコーディング方法については明言は避けますがDjango特有の書き方については記載します。
HTML内に{{ form }}の記載があると思いますが、これを利用することでカスタムユーザーモデルで設定したユーザー名やemail,passwordなどのユーザーが入力するためのフォームが作成されます。またform内にはerrorやlabelも含まれており、for文を利用することで一つずつ取り出しています。


```Python:ディレクトリ
app_sns/
        ├プロジェクト
        ├ manage.py
        └ accounts(app_name)
        └ timelines(app_name)
        └ templates/#追加
                 └ base.html
                 └ signup.html
                 └ signup_done.html
                 └ login.html
                 └ logout.html
                 └ password_reset.html
                 └ passowrd_reset_sent.html
                 └ passowrd_reset_form.html
                 └ password_reset_done.html
```

コードは以下です。リンクで#にしている部分はtimelinesで実装します。

```HTML:base.html(すべての機能に使います)
{% load static %}
<!DOCTYPE html>
<html lang = 'ja'>
<head>
    <meta charset="UTF-8" />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <link rel = 'stylesheet' href = '{% static 'css/style1.css' %}'>
</head>
<body>
    <header>
        <div class = 'header-nav'>
              <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
                {% if user.is_authenticated %}
                <div class="container-fluid">
                  <a class="navbar-brand" href="#">
                    SNSAPP~no-title~
                  </a>
                  <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                      <li class="nav-item">
                        <a class="nav-link active" aria-current="page" href="#">タイムライン</a>
                      </li>
                      <li class="nav-item">
                        <a class="nav-link active" href="#">新規投稿</a>
                      </li>
                      <li class="nav-item">
                        <a class="nav-link active" href="{% url 'accounts:logout' %}">ログアウト</a>
                      </li>
                    </ul>
                  </div>
                </div>
                {% else %}
                <div class="container-fluid">
                    <a class="navbar-brand" href="#}">
                      SNSAPP~no-title~
                    </a>
                    <div class="collapse navbar-collapse" id="navbarSupportedContent">
                      <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                        <li class="nav-item">
                          <a class="nav-link active" aria-current="page" href="{% url 'accounts:signup' %}">登録</a>
                        </li>
                      </ul>
                    </div>
                  </div>
                {% endif %}
              </nav>
        </div>
        
        
        
    </header>
    <main>{% block contents %}{% endblock %}</main> 
    <footer></footer>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
    <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
    
</body>
</html>
```

```{% if user.is_authenticated %}について
{% if user.is_authenticated %}
「ログインしている場合に表示する機能」
{% else %}
「ログインしていない場合に表示」←　まだここのないようがヘッダー部に表示されているはずです。
{% endif %}

```

```HTML:signup.html
{% extends 'base.html' %}
{% load static %}

{% block contents %}

<div class = 'form'>
    <form method = 'post'>
        {% csrf_token %}
        <h1 style = 'text-align:center; border-bottom: 1px solid black;'>Signup Form</h1>
        {% for fields in form %}
        <div class = 'userinfo'>
            <div class = 'info'>
                <span>必須</span>
                    {{ fields.label_tag }}
                    <br>
                    <p>
                    {{ fields }}
                    </p>

                    <div class = 'help_text'>
                    {% if fields.help_text %}
                        <small style = 'color: gray'>{{ fields.help_text }}</small>
                    {% endif %}
                    </div>
            </div>
            {% for error in fields.errors %}
                <p style = 'color: red'>{{ error }}</p>
            {% endfor %}
            </p>
        </div>
        {% endfor %}
        <input class = 'btn_auth' type = 'submit' value = '登録'>
        <button class = 'btn-auth' onClick = "location.href = '#'">トップページへ戻る</a>
    </form>
    
</div>

{% endblock %}
```

```HTML:signup_done.html
{% extends 'base.html' %}
{%load static %}

{% block contents %}
<h3>登録できました！！</h3>

<p><a href = '#'>ログインはこちら</a></p>
{% endblock %}

```

```HTML:login.html
{% extends 'base.html'%}

{% load static %}

{% block contents %}
<div class = 'inputarea'>
    {% if form.errors %}
        <p style='color: red'>ユーザー名もしくはパスワードに誤りがあります。
            <br>もう一度確認してください。
        </p>
    {% endif %}
    <div class = "form">
    <form method = 'post'>
        {% csrf_token %}
        <h1>Please Login</h1>
        <div class = 'userinfo'>
            <div class = 'info'>
            <label for = 'Username'><span>必須</span>ユーザー名</label>
            <input type = 'text' name = 'username' id = 'id_username' maxlength ='100' autocapitalize = 'none' autocomplete = 'username' class = 'form-control' placeholder = 'ユーザー名' required autofocus>
            </div>
            <div class = 'info'>
            <label for = 'Password'><span>必須</span>Password</label>
            <input type = 'password' name = 'password' id = 'id_password' autocomplete = 'current-password' class = 'form-control' placeholder = 'パスワード' required>
            </div>
        </div>
        <input type = 'submit' class = 'btn_auth' value = 'ログイン'>
        <p><a href = '{% url 'password_reset' %}'>パスワードを忘れましたか？</a></p>
        <input type = 'hidden' name = 'next' value = '#}'>
    </form>
</div>
</div>
{% endblock %}
```

```HTML:logout.html
{% extends 'base.html' %}
{% load static %}

{% block contents %}
<div class = 'top-page'>
    <div class = 'top-container'>
        <h2>SNSAPP~no-title~へようこそ</h2>

        <h5>日常の緩い出来事やポジティブな出来事を発信しよう！！</h5>
        <div class = 'logout'>
        <h3 style = 'color: red;'>ログアウトしました</h3>
        <button class = 'btn-auth' onClick = "location.href = '#'">トップページへ</button>
        </div>
    </div>
</div>

{% endblock %}
```
パスワードリセットは計4ページで構成されていrます。
①リセットフォームで登録アドレスに送信
②送信完了ページ
③送信されたメールに記載されているリンクから新しいパスワードを入力するページ
④リセット完了ページ

```HTML:passowrd_reset.html
{% extends 'base.html' %}

{% block contents %}

<div class = 'inputarea'>
    {% if form.errors %}
        <p style='color: red'>ユーザー名もしくはパスワードに誤りがあります。
            <br>もう一度確認してください。
        </p>
    {% endif %}
    <div class = "form">
    <form method = 'post'>
        {% csrf_token %}
        <h1>Password Reset</h1>
        <h4>Send Reset email</h4>
        <div class = 'userinfo'>
            <div class = 'info'>
                {{ form }}
            </div>
            <input type = 'submit' value = '送信' class = 'btn-auth'>
        </div>
    </form>
    <p><a href = '#'>トップページへ戻る</a></p>
</div>
</div>

{% endblock %}
```

```HTML:password_reset_sent.html
{% extends 'base.html' %}
{% block contents %}
<div class = 'form'>
    <div class = 'userinfo'>
        <div class = 'info'>
            <h3>メールを送信しました。</h3>
            <h5 style = 'float:left;'>届かない場合はメールアドレスが正しか確認してください。</h5>
        </div>
        <br>
        <button class='btn_auth' onClick="location.href = '{% url 'accounts:login' %}'">ログインページへ戻る</button>
    </div>
</div>
{% endblock %}
```
```HTML:password_reset_form.html
{% extends 'base.html' %}
{% block contents %}

<div class = 'inputarea'>
    {% if form.errors %}
        <p style='color: red'>ユーザー名もしくはパスワードに誤りがあります。
            <br>もう一度確認してください。
        </p>
    {% endif %}
    <div class = "resetform">
    <form method = 'post'>
        {% csrf_token %}
        <h1>Password Reset</h1>
        <div class = 'userinfo'>

                <p>
                {{ form }}
                </p>
            
            <input type = 'submit' value = 'リセット' class = 'btn-auth'>
        </div>
    </form>
    <button class='btn_auth' onClick="location.href = '{% url 'accounts:login' %}'">ログインページへ</button>
</div>
</div>

{% endblock %}
```

```HTML:passowrd_reset_done.html
{% extends 'base.html' %}

{% block contents %}
<div style = 'text-align:center'>
<h2 style = 'text-align:center'>パスワードがリセットされました。</h2>

<button class='btn_auth' onClick="location.href = '{% url 'accounts:login' %}'">ログインページへ戻る</button>
</div>
{% endblock %}
```

<h5>settings.pyについて</h5>
settings.pyにパスワードリセット時のメール送信のために設定をしましょう。

```Python:settings.py
#一番下に追記

#CSS設定
STATIC_URL = '/static/'

STATIFIERS_DIR = STATICFILES_DIRS = (
    [os.path.join(BASE_DIR, 'static'),]
)

#カスタムユーザーモデルを使うための設定(アプリ名.モデル名)
AUTH_USER_MODEL = 'accounts.User'

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
DEFAULT_FROM_EMAIL = 'xxxx@gmail.com'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'xxxxx@gmail.com'
EMAIL_HOST_PASSWORD = 'xxxxxxx'#Google設定から取得できます。
EMAIL_USE_TLS = True
```

<h2>まとめ</h2>
ここまでで、ユーザー認証まわりの設定が完了しました。

次回はタイムライン[機能の実装](https://qiita.com/Taku_nakahara/items/f028bbdc966a04c968e3)です。SNSアプリケーションを完成させましょう！