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.py
にSTATICFILES_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.py
にUser
モデルを登録します。<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
を継承すると、ログイン前に直接その画面にアクセスした場合、ログイン画面にリダイレクトするようにできます。Sign up here with your email
ConversionConversion EmoticonEmoticon