😎

Djangoのユーザ認証を試してみた

2022/05/03に公開

概要

PythonのフルスタックWEBフレームワークのDjangoのユーザ認証機能を試したくて、簡単なToDoリストを作ってみましたので、それを紹介します。
本記事の前提知識は、Djangoのチュートリアルに相当する内容です。
また、Djangoのユーザ認証機能については、公式のドキュメントを参考にしています。
なお、作成したアプリはGitHubのリポジトリに置いています。

アプリの概要としては下記のとおりです。

  • ユーザごとに複数のタスクを登録できます。
  • タスクの確認・更新・削除は、登録したユーザのみ行えます。
  • ユーザ認証のために、登録・ログイン・ログアウトのページがあります。

タスクの編集画面は、htmlやcssを工夫していないので分かりにくいですが、下記の画像のとおりです。

Djangoの認証機能

ユーザの作成

django.contrib.auth.models.UserがDjangoのユーザモデルで、主要な属性は以下のとおりです。

  • username
  • password
  • email

なお、usernameとpasswordは必須項目です。

ユーザを作成するには下記の通り、create_user()関数を使います。

from django.contrib.auth.models import User
user = User.objects.create_user('username', 'foo@example.com', 'password')

ユーザ認証

DjangoはHTTPリクエストに対して認証の有無を判別します。
リクエストrequestには現在のユーザーを示すrequest.user属性が付与されています。
もしユーザーが現在ログインしていない場合、この属性にはAnonymousUserのインスタンスが、ログインしている場合はUserのインスタンスがセットされます。
この二者はis_authenticatedを用いて次のように識別する事ができます。

if request.user.is_authenticated:
    # Do something for authenticated users.
    ...
else:
    # Do something for anonymous users.
    ...

また、ログインしているユーザが、あるユーザfoo_userかどうか識別するには下記のようにします。

if request.user == foo_user:
    # Do something for foo_user.
    ...

ログイン

ログインするにはlogin()関数を使います。
usernameとpasswordはPOSTリクエストから取得することを想定しています。

from django.contrib.auth import authenticate, login

def login_view(request):
    username = request.POST['username']
    password = request.POST['password']
    user = authenticate(request, username=username, password=password)
    if user is not None:
        login(request, user)
        # Redirect to a success page.
        ...
    else:
        # Return an 'invalid login' error message.
        ...

ログアウト

ログアウトするにはlogout()関数を使います。

from django.contrib.auth import logout

def logout_view(request):
    logout(request)
    # Redirect to a success page.

ToDoリストアプリの紹介

ToDoリストアプリの概要としては下記のとおりです。

  • ユーザごとに複数のタスクを登録できます。
  • タスクの確認・更新・削除は、登録したユーザのみ行えます。
  • ユーザ認証のために、登録・ログイン・ログアウトのページがあります。

タスクモデルからDjangoユーザモデルを参照する

タスクモデルはDjangoユーザとタスク名のみ属性を持ちます。
タスクモデルからDjangoユーザモデルを参照するには、下記の通りdjango.conf.settings.AUTH_USER_MODELを外部参照キーにします。
なお、name属性をユニークにしているのは、本記事に関係ない事情で設定時の挙動を試したかったからなので、気にしないでください。

todo_list/models.py
from django.conf import settings
from django.db import models


class Task(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    name = models.CharField(max_length=255, unique=True)

    def __str__(self):
        return self.name

タスクのCRUDに関するユーザ認証

タスクの読取

第三者にタスクが見られないようにします。
tasks = Task.objects.filter(user=request.user)により、ログインユーザに紐づくタスクのみを抽出し、表示関数render()に渡しています。

todo_list/views.pyの抜粋
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import render

from .models import Task


def index(request: HttpRequest) -> HttpResponse:
    if not request.user.is_authenticated:
        return HttpResponseRedirect('login/')

    tasks = Task.objects.filter(user=request.user)
    return render(
        request,
        'todo_list/todo_list.html',
        {'username': request.user.username, 'tasks': tasks}
    )

HTMLのテンプレートファイルは下記のとおりです。

todo_list/templates/todo_list/register.html
<h1>ToDo List of {{ username }}</h1>

{% if tasks %}
    <ul>
    {% for task in tasks %}
        <li>
        <div style="display:inline-flex">
            <form action="/task/update/" method="post">
                {% csrf_token %}
                <input type="hidden" name="id" value="{{ task.id }}">
                <input type="text" name="name" value="{{ task.name }}" required>
                <input type="submit" value="update">
            </form>
            <form action="/task/delete/" method="post">
                {% csrf_token %}
                <input type="hidden" name="id" value="{{ task.id }}">
                <input type="submit" value="delete">
            </form>
        </div>
        </li>
    {% endfor %}
    </ul>
{% else %}
    <p>No task is registered.</p>
{% endif %}

<form action="/task/create/" method="post">
    {% csrf_token %}
    New task:
    <input type="text" name="name" required>
    <input type="submit" value="add">
</form>

<form action="/logout/" method="post">
    {% csrf_token %}
    <input type="submit" value="logout">
</form>

タスクの作成

タスクの作成は、ログインしているユーザのみ可能です。
タスク作成時はHTMLのフォームを通して、POSTリクエストをcreate_task()に渡します。
create_task()では最初にcheck_task_request()によって、POST以外のリクエストや、未ログイン時のリクエストを拒否しています(404応答を返します)。
また、POSTリクエスト内のname属性をもとにタスクを作成し、最後にタスク表示ページへリダイレクトします。

todo_list/views.pyの抜粋
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect
from .models import Task


def check_task_request(request: HttpRequest) -> None:
    if request.method != 'POST':
        raise Http404('Request method is not POST.')

    user=request.user
    if not user.is_authenticated:
        raise Http404('User is not authenticated.')


def create_task(request: HttpRequest) -> HttpResponse:
    check_task_request(request=request)

    name = request.POST.get('name')
    if name is None:
        Http404('Task name is not set.')

    task = Task(user=request.user, name=name)
    task.save()
    return HttpResponseRedirect('../../')

タスクの更新と削除

タスクの更新と削除は、そのタスクを作成したユーザのみ可能です。
タスク更新と削除ははHTMLのフォームを通して、POSTリクエストをそれぞれupdate_task()delete_task()に渡します。
タスク作成と同じように、最初にcheck_task_request()によって、POST以外のリクエストや、未ログイン時のリクエストを拒否しています(404応答を返します)。
また、POSTリクエスト内のid属性をもとに更新・削除したいタスクを特定します。
if task.user != request.userの部分でそのタスクに紐づくユーザとログインユーザの一致を確認し、違う場合に404応答を返します。
問題なければ、更新や削除を行いタスク表示ページへリダイレクトします。

todo_list/views.pyの抜粋
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect
from .models import Task


def check_task_request(request: HttpRequest) -> None:
    if request.method != 'POST':
        raise Http404('Request method is not POST.')

    user=request.user
    if not user.is_authenticated:
        raise Http404('User is not authenticated.')


def update_task(request: HttpRequest) -> HttpResponse:
    check_task_request(request=request)

    id_ = request.POST.get('id')
    name = request.POST.get('name')
    if id is None or name is None:
        Http404('Task id or name is not set.')

    task = Task.objects.get(pk=id_)
    if task.user != request.user:
        raise Http404("Another user's task can't be edited.")

    task.name = name
    task.save()
    return HttpResponseRedirect('../../')


def delete_task(request: HttpRequest) -> HttpResponse:
    check_task_request(request=request)

    id_ = request.POST.get('id')
    if id is None:
        Http404('Task id is not set.')

    task = Task.objects.get(pk=id_)
    if task.user != request.user:
        raise Http404("Another user's task can't be edited.")

    task.delete()
    return HttpResponseRedirect('../../')

まとめ

Djangoのユーザ認証機能を試すために作成したToDoリストアプリを紹介しました。

PythonでWEBアプリ開発というとDjango以外にもFlaskなどが選択肢に入ります。
Flaskの場合データベース連携や認証はプラグインのパッケージを導入する必要が有り、その選定が面倒だということでDjangoを選びました。
実際に使ってみましたが、大きく戸惑うことなく利用できました。

Django自体使ってみて一週間も立っておらず、イマイチなコードになっているかもしれませんので、お気づきになられた方はコメント等いただけると嬉しいです。

GitHubで編集を提案

Discussion