Python + Djangoでメールアドレスを使ったログイン認証



Django にはデフォルトでログイン認証機能が備わっているとのことなので試してみました。
ユーザー名とパスワードでのログインがデフォルトなのですが、今回はカスタマイズしてメールアドレスでログインできるようにもしました。





*今回作る画面



























































*参考



*環境

  • Python 3.6.3
  • Django 2.2.2


*環境構築

任意の作業フォルダを用意し、仮想環境を作成して Django をインストールします。
$ mkdir django-auth
$ cd django-auth/
$ python3 -m venv env
$ source env/bin/activate

(env)$ pip install django


*プロジェクト作成

作業フォルダで下記コマンドを実行します。
startprojectのうしろは任意のプロジェクト名を指定します。(今回はmysiteにしました)
$ django-admin startproject mysite

下記構成のファイルが作成されます。
mysite
├── manage.py
└── mysite
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

試しにアプリケーションを起動します。
$ python manage.py runserver

下記URLにアクセスすると Django のサンプル画面が表示されます。
http://127.0.0.1:8000/













*ログイン画面の作成

Djangoではデフォルトでアカウント認証機能が用意されているので、その機能を使えるようにurls.pyにパスを追加します。
<mysite/mysite/urls.py>
from django.contrib import admin
from django.urls import path
from django.urls import path, include   <-- 追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),   <-- 追加
]

ログイン画面のテンプレートを作成します。
テンプレートファイルはプロジェクト直下にtemplatesディレクトリを作成し、その中に作成します。
ログイン用のテンプレートはregistrationという名前でディレクトリを作成し、その中にlogin.htmlを配置する必要があります。
<mysite/templates/registration/login.html>
<!-- templates/registration/login.html -->
<h2>Login</h2>
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Login</button>
</form>

templatesディレクトリを読み込めるようにsettings.pyを修正します。
また、ログイン後にリダイレクトされるようLOGIN_REDIRECT_URLを追加します。
<mysite/mysite/settings.py>
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],   <-- 追加
        'APP_DIRS': True,
        ...
    },
]

...

LOGIN_REDIRECT_URL = '/'    <-- 追加


*アカウント登録画面の実装

アプリケーションを作成します。
$ python manage.py startapp app

下記構成でファイルが作成されます。
mysite
├── mysite/
└── app/
 ├── __init__.py
 ├── admin.py
 ├── apps.py
 ├── migrations/
 |   └── __init__.py
 ├── models.py
 ├── tests.py
 └── views.py

アカウント作成に使う Form クラスを作成します。
<mysite/app/forms.py>
from django.forms import EmailField

from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User


class SignUpForm(UserCreationForm):
    email = EmailField(label=_('メールアドレス'), required=True, help_text=_('Required.'))

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

    def save(self, commit=True):
        user = super(SignUpForm, self).save(commit=False)
        user.email = self.cleaned_data['email']
        if commit:
            user.save()
        return user

アカウント登録時に呼び出す view クラスを作成します。
<mysite/app/views.py>
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
from django.views.generic import CreateView
from django.contrib.auth import login, authenticate
from django.shortcuts import render, redirect
from app.forms import SignUpForm

class SignUpView(CreateView):
    def post(self, request, *args, **kwargs):
        form = SignUpForm(data=request.POST)
        if form.is_valid():
            form.save()
            username = form.cleaned_data.get('username')
            email = form.cleaned_data.get('email')
            password = form.cleaned_data.get('password1')
            user = authenticate(username=username, email=email, password=password)
            login(request, user)
            return redirect('/')
        return render(request, 'create.html', {'form': form})

    def get(self, request, *args, **kwargs):
        form = SignUpForm(request.POST)
        return render(request, 'create.html', {'form': form})

    def form_valid(self, form):
        user = form.save()
        login(self.request, user)
        self.object = user
        return HttpResponseRedirect(self.get_success_url())

共通で使うCSSファイルを作成します。
<mysite/static/css/style.css>
.auth-form {
  font-size: 14px;
  font-family: Roboto,sans-serif;
  max-width: 400px;
}
.auth-form label {
  margin-bottom: 0;
}
.auth-form input[type='text'], input[type='password'], input {
  font: 15px/24px sans-serif;
  box-sizing: border-box;
  width: 100%;
  padding: 0.3em;
  transition: 0.3s;
  letter-spacing: 1px;
  color: #606060;
  border-radius: 0.25rem;
}
.login-error-text {
  color: #ff0000;
  font-size: 12px;
  padding: 1px 0 5px 0;
}
.login-text-div {
  padding: 12px;
}
.login-error-text ul {
  list-style-type: none;
  padding: 0;
  margin-bottom: 0;
}
.alert-text {
  font-size: 14px;
}
.auth-button {
  margin-top: 10px;
}
.logout-content {
  text-align: center;
}

CSSファイルを読み込めるようsettings.pySTATICFILES_DIRSの設定を追加します。
<mysite/mysite/settings.py>
...

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

テンプレートファイルを複数作る場合、共通部分はbase.htmlとして抜き出しておきます。
<mysite/templates/base.html>
<!-- templates/base.html -->
{% load static %}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <link href="{% static 'css/style.css' %}" rel="stylesheet" type="text/css">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
  <link href="https://getbootstrap.com/docs/4.1/examples/sign-in/signin.css" rel="stylesheet">
<title>{% block title %}Django Auth Tutorial{% endblock %}</title>
</head>
<body>
  <div class="container">
    <div class="content">
      {% block content %}
      {% endblock %}        
    </div>  
  </div>
  </main>
</body>
</html>

ログイン画面を修正します。
{% extends "base.html" %}をファイルの先頭に書くことで、base.htmlを読み込むことができます。
<mysite/templates/registration/login.html>
{% extends "base.html" %}
{% load static %}
{% block title %}Login{% endblock %}
{% block content %}
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-lg-4 col-md-6 col-sm-8">
                <div class="card">
                    <div class="card-body">
                        <h3 class="card-title">Log in</h3>
                        {% csrf_token %}
                        {% if form.non_field_errors %}
                            <div class="alert alert-danger alert-text" role="alert">
                                {% for error in form.non_field_errors %}
                                    <p{% if forloop.last %} class="mb-0"{% endif %}>{{ error }}</p>
                                {% endfor %}
                            </div>
                        {% endif %}
                        <form method="post" action="{% url 'login' %}" class="auth-form" novalidate>
                            {% csrf_token %}
                            {% for field in form %}
                            <div>
                                {{ field.label_tag }}
                                {{ field }}
                                {% if field.errors %}
                                    <div class="login-error-text">
                                        {{ field.errors }}
                                    </div>
                                {% else %}
                                    <div class="login-text-div"></div>
                                {% endif %}
                            </div>
                            {% endfor %}
                            <input type="submit" value="login" class="form-control btn-primary auth-button" />
                            <input type="hidden" name="next" value="{{ next }}" />
                        </form>
                    </div>
                    <div class="card-footer text-muted text-center">
                        <a href="{% url 'create_account' %}">Sign up</a>
                    </div>
                </div>
                <div class="text-center py-2">
                    <small>
                        <a href="{% url 'password_reset' %}" class="text-muted">Forgot your password?</a>
                    </small>
                </div>
            </div>
        </div>
    </div>
{% endblock %}

アカウント登録画面を新規作成します。
<mysite/templates/create.html>
{% extends "base.html" %}
{% block title %}Signup{% endblock %}
{% block content %}
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-lg-5 col-md-7 col-sm-9">
                <div class="card">
                    <div class="card-body">
                        <h3 class="card-title">Sign up</h3>
                        {% if form.non_field_errors %}
                            <div class="alert alert-danger alert-text" role="alert">
                                {% for error in form.non_field_errors %}
                                    <p{% if forloop.last %} class="mb-0"{% endif %}>{{ error }}</p>
                                {% endfor %}
                            </div>
                        {% endif %}
                        <form method="POST" action="{% url 'create_account' %}" class="auth-form" novalidate>
                            {% csrf_token %}
                            {% for field in form %}
                            <div>
                                {{ field.label_tag }}
                                {{ field }}
                                {% if field.errors %}
                                    <div class="login-error-text">
                                        {{ field.errors }}
                                    </div>
                                {% else %}
                                    <div class="login-text-div"></div>
                                {% endif %}
                            </div>
                            {% endfor %}
                            <button type="submit" class="form-control btn-primary auth-button">登録</button>
                        </form>
                    </div>
                    <div class="card-footer text-muted text-center">
                        Already have an account? <a href="{% url 'login' %}">Log in</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
{% endblock %}

ログイン後に遷移するホーム画面を作成します。
今回はシンプルにログインユーザー名を表示し、ログアウト用のリンクを付けただけの画面です。
<mysite/templates/home.html>
<!-- templates/home.html-->
{% extends 'base.html' %}
{% block title %}Home{% endblock %}
{% block content %}
{% if user.is_authenticated %}
  Hi {{ user.username }}!
  <p><a href="{% url 'logout' %}">logout</a></p>
{% else %}
  <p>You are not logged in</p>
  <a href="{% url 'login' %}">login</a>
{% endif %}
{% endblock %}

アカウント登録画面と、ホーム画面のパスを追加します。
<mysite/mysite/urls.py>
from django.contrib import admin
from django.urls import path
from django.urls import path, include
from django.views.generic.base import TemplateView  <-- 追加
from app import views  <-- 追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),
    path('', TemplateView.as_view(template_name='home.html'), name='home'),  <-- 追加
    path('create_account', views.SignUpView.as_view(), name='create_account')  <-- 追加
]

ログアウト時のリダイレクトURLを設定します。
ついでに設定を日本に修正します。
<mysite/mysite/urls.py>
LANGUAGE_CODE = 'ja-jp'    <-- 修正

TIME_ZONE = 'Asia/Tokyo'   <-- 修正

USE_TZ = False             <-- 修正

LOGOUT_REDIRECT_URL = '/'  <-- 追加

DBを作成し直す必要があるため、下記コマンドを実行してDBをマイグレーションします。
$ python manage.py makemigrations
$ python manage.py migrate

アプリケーションを実行します。
$ python manage.py runserver























*メールアドレスでログイン

デフォルトの状態だとユーザー名でログインするようになっているのですが、メールアドレスでログインする場合はUserテーブルをカスタマイズする必要があります。
AbstractUserを継承したUserクラス、UserManagerを継承したManagerクラスを作成してメールアドレスを受け取るようにします。
<mysite/app/models.py>
from django.contrib.auth.models import AbstractUser, UserManager
from django.db import models
from django.utils.translation import gettext_lazy as _


class Manager(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)


class User(AbstractUser):
    username = models.CharField(_('username'), unique=True, max_length=150, blank=False)
    email = models.EmailField(_('email address'), unique=True)

    objects = Manager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

settings.pyに設定を追加して、Userモデルを置き換えます。
<mysite/mysite/settings.py>
...

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app',     <-- 追加
]

...

AUTH_USER_MODEL = 'app.User'    <-- 追加

admin.pyUserモデルを登録します。
<mysite/app/admin.py>
from django.contrib import admin

from .models import User     <-- 追加

admin.site.register(User)    <-- 追加

Userモデルをカスタマイズしたので、forms.pyに追加していたEmailFieldを削除します。
アカウント登録画面の初期表示時に、ログインに必要になるメールアドレスの項目に最初にカーソルがあたってしまうため、画面に表示するフィールドの順番を変更しています。(ユーザー名でなくメールアドレスに変更)
<mysite/app/forms.py>
from django.forms import EmailField

from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.forms import UserCreationForm
from .models import User


class SignUpForm(UserCreationForm):

    class Meta:
        model = User
        # ↓ username と email の順番を入れ替え
        fields = ('email', 'username','password1', 'password2')

    def save(self, commit=True):
        user = super(SignUpForm, self).save(commit=False)
        if commit:
            user.save()
        return user

models.pyからemailの項目を削除したので、views.pyからも削除します。
<mysite/app/views.py>
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect

from django.views.generic import CreateView
from django.contrib.auth import login, authenticate
from django.shortcuts import render, redirect
from app.forms import SignUpForm

class SignUpView(CreateView):
    def post(self, request, *args, **kwargs):
        form = SignUpForm(data=request.POST)
        if form.is_valid():
            form.save()
            email = form.cleaned_data.get('email')
            password = form.cleaned_data.get('password1')
            user = authenticate(email=email, password=password)
            login(request, user)
            return redirect('/')
        return render(request, 'create.html', {'form': form})

    def get(self, request, *args, **kwargs):
        form = SignUpForm(request.POST)
        return render(request, 'create.html', {'form': form})

    def form_valid(self, form):
        user = form.save()
        login(self.request, user)
        self.object = user
        return HttpResponseRedirect(self.get_success_url())

Userクラスをカスタマイズしたので、DBをマイグレーションし直します。
下記コマンドを実行すると、マイグレーションファイルが作成されます。
$ python manage.py makemigrations

Migrations for 'app':
app/migrations/0001_initial.py
- Create model User

このままマイグレーションするとエラーになるため、下記をコメントアウトします。
<mysite/mysite/settings.py>
...
INSTALLED_APPS = [
#    'django.contrib.admin',   <-- コメントアウト
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app',
]

<mysite/mysite/urls.py>
# from django.contrib import admin   <-- コメントアウト
from django.urls import path
from django.urls import path, include
from django.views.generic.base import TemplateView
from app import views

urlpatterns = [
#    path('admin/', admin.site.urls),   <-- コメントアウト
    path('accounts/', include('django.contrib.auth.urls')),
    path('', TemplateView.as_view(template_name='home.html'), name='home'),
    path('create_account', views.SignUpView.as_view(), name='create_account')
]

下記コマンドを実行します。
OKを表示されたら成功しているので、コメントアウトした箇所を元に戻します。(元に戻さないと、パスワードを忘れた場合のリンク先になる管理画面が表示できなくなります)
$ python manage.py migrate

Operations to perform:
  Apply all migrations: app, auth, contenttypes, sessions
Running migrations:
  Applying app.0001_initial... OK

アプリケーションを実行します。
$ python manage.py runserver

下記にアクセスすると画面が表示されます。
http://127.0.0.1:8000/

























*所感

ログイン用の認証機能や、入力フォームの細かいバリデーション機能がデフォルトで備わっているのは便利でしたが、どこで何をしているのかパッと見ただけではわからず、仕組みの中身を理解するのに時間がかかりました。また、メールアドレスでログインしたい場合や、画面初期表示時にカーソルが当たる項目を変更するといった独自のカスタマイズをしたい場合は結構面倒だったので、Flaskなど拡張しやすいフレームワークを使ったほうが柔軟に実装できそうです。
Djangoは一般的な共通機能が備わっていてお作法を理解すれば速く実装できる、誰が書いても整理された実装になるので保守性が上がるといったメリットはありますが、小さいアプリケーションや独自にカスタマイズしたい場合には向いていないかと感じました。

ちなみにviews.pyのクラスでfrom django.contrib.auth.mixins import LoginRequiredMixinを継承すると、ログイン前に直接その画面にアクセスした場合、ログイン画面にリダイレクトするようにできます。

Previous
Next Post »

人気の投稿