【Django】ユーザー認証機能を実装する|簡単な掲示板アプリの作成

Djangoが提供するユーザー認証システムを使い、Webアプリに必須のサインアップ、ログイン、ログアウト機能を実装します。ログインしていないと投稿や閲覧ができないようにアクセスを制限する方法や、ログイン状態に応じてページの表示を動的に切り替える方法まで、初心者にも分かりやすく解説します。

作成日: 更新日:

開発環境

  • OS: Windows10
  • Visual Studio Code: 1.73.0
  • Python: 3.10.11
  • Django: 5.0.3

サンプルコード

/app/forms.py

/app/forms.py
1 # app/forms.py
2 from django import forms
3 from .models import Board
4+from django.contrib.auth.forms import UserCreationForm
5+from django.contrib.auth.models import User
6 
7 class BoardForm(forms.ModelForm):
8     class Meta:
9         model = Board
10         fields = ['title', 'content']
11+
12+class SignUpForm(UserCreationForm):
13+    email = forms.EmailField(max_length=254, help_text='Required. Inform a valid email address.')
14+
15+    class Meta:
16+        model = User
17+        fields = ['username', 'email', 'password1', 'password2']
18

/app/urls.py

/app/urls.py
1 # app/urls.py
2 from django.urls import path
3+from django.contrib.auth import views as auth_views
4+from django.contrib.auth.decorators import login_required
5 from . import views
6 
7 urlpatterns = [
8     path('', views.index, name='index'),
9-    path('new/', views.new, name='new'),
10-    path('create/', views.create, name='create'),
11-    path('<int:pk>/', views.show, name='show'),
12-    path('<int:pk>/edit/', views.edit, name='edit'),
13-    path('<int:pk>/update/', views.update, name='update'),
14-    path('<int:pk>/delete/', views.delete, name='delete'),
15+    path('new/', login_required(views.new), name='new'),
16+    path('create/', login_required(views.create), name='create'),
17+    path('<int:pk>/', login_required(views.show), name='show'),
18+    path('<int:pk>/edit/', login_required(views.edit), name='edit'),
19+    path('<int:pk>/update/', login_required(views.update), name='update'),
20+    path('<int:pk>/delete/', login_required(views.delete), name='delete'),
21 ]
22

/app/views.py

/app/views.py
1 # app/views.py
2 from django.shortcuts import render, redirect, get_object_or_404
3 from .models import Board
4-from .forms import BoardForm
5+from .forms import BoardForm, SignUpForm
6+from django.contrib.auth import logout
7+from django.contrib.auth.views import LoginView
8+from django.contrib.auth.decorators import login_required
9 
10 def index(request):
11     boards = Board.objects.all().order_by('-updated_at')
12     return render(request, 'index.html', {'boards': boards})
13 
14+@login_required
15 def new(request):
16     form = BoardForm()
17     return render(request, 'new.html', {'form': form})
18 
19+@login_required
20 def create(request):
21     if request.method == 'POST':
22         form = BoardForm(request.POST)
23         form = BoardForm()
24     return render(request, 'new.html', {'form': form})
25 
26+@login_required
27 def show(request, pk):
28     board = Board.objects.get(pk=pk)
29     return render(request, 'show.html', {'board': board})
30 
31+@login_required
32 def edit(request, pk):
33     board = Board.objects.get(pk=pk)
34     form = BoardForm(instance=board)
35     return render(request, 'edit.html', {'form': form, 'board': board})
36 
37+@login_required
38 def update(request, pk):
39     board = Board.objects.get(pk=pk)
40     if request.method == 'POST':
41         form = BoardForm(instance=board)
42     return render(request, 'edit.html', {'form': form, 'board': board})
43 
44+@login_required
45 def delete(request, pk):
46     board = get_object_or_404(Board, pk=pk)
47     if request.method == 'POST':
48         board.delete()
49         return redirect('index')
50-    return redirect('index', pk=pk)
51+    return redirect('index', pk=pk)
52+
53+# ログインページのビュー
54+class CustomLoginView(LoginView):
55+    template_name = 'registration/login.html'
56+
57+# ログアウト
58+def logout_view(request):
59+    logout(request)
60+    return redirect('index')
61+
62+# サインアップページのビュー
63+def signup(request):
64+    if request.method == 'POST':
65+        form = SignUpForm(request.POST)
66+        if form.is_valid():
67+            form.save()
68+            return redirect('login')
69+    else:
70+        form = SignUpForm()
71+    return render(request, 'registration/signup.html', {'form': form})
72+
73+# プロフィールページのビュー
74+@login_required
75+def profile(request):
76+    user = request.user
77+    context = {
78+        'user': user
79+    }
80+    return render(request, 'accounts/profile.html', context)
81

/config/urls.py

/config/urls.py
1 """
2 from django.contrib import admin
3 from django.urls import path, include
4+from django.contrib.auth import views as auth_views
5+from app import views
6 
7 urlpatterns = [
8     path("admin/", admin.site.urls),
9     path('', include('app.urls')),
10+    path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
11+    path('accounts/logout/', views.logout_view, name='logout'),
12+    path('accounts/signup/', views.signup, name='signup'),
13+    path('accounts/profile/', views.profile, name='profile'),
14 ]
15

/templates/accounts/profile.html

/templates/accounts/profile.html
1+{% extends 'base.html' %}
2+
3+{% block title %}プロフィール画面{% endblock %}
4+
5+{% block content %}
6+    <section class="container">
7+        <div class="row justify-content-center">
8+            <div class="col-md-6">
9+                <h2 class="mb-4">プロフィール画面</h2>
10+                <p>ユーザー名: {{ user.username }}</p>
11+                <p>メールアドレス: {{ user.email }}</p>
12+            </div>
13+        </div>
14+        <a href="{% url 'index' %}" class="btn btn-primary">一覧に戻る</a>
15+    </section>
16+{% endblock %}
17

/templates/base.html

/templates/base.html
1     <!-- ヘッダーの内容 -->
2     <header class="bg-success">
3         <div class="container">
4-            <div class="pt-3 pb-1 mb-2 text-white">
5+            <div class="pt-3 pb-1 mb-2 text-white d-flex justify-content-between align-items-center flex-wrap">
6                 <h1 class="display-4">掲示板アプリ</h1>
7+                {% if user.is_authenticated %}
8+                    <!-- ログインしている場合 -->
9+                    <div>
10+                        <p class="text-light">ようこそ {{ user.username }} さん</p>
11+                    </div>
12+                    <div>
13+                        <form action="{% url 'logout' %}" method="post" style="display: inline;">
14+                            {% csrf_token %}
15+                            <button type="submit" class="btn btn-light">ログアウト</button>
16+                        </form>
17+                    </div>
18+                    {% else %}
19+                    <!-- ログインしていない場合 -->
20+                    <div>
21+                        <a href="{% url 'signup' %}" class="btn btn-light">サインアップ</a>
22+                        <a href="{% url 'login' %}" class="btn btn-light">ログイン</a>
23+                    </div>
24+                {% endif %}
25+                </div>
26             </div>
27         </div>
28     </header>
29

/templates/registration/login.html

/templates/registration/login.html
1+{% extends 'base.html' %}
2+
3+{% block title %}ログイン画面{% endblock %}
4+
5+{% block content %}
6+    <section class="container">
7+        <div class="row justify-content-center">
8+            <div class="col-md-6">
9+                <h2 class="mb-4">ログイン画面</h2>
10+                <form method="post">
11+                    {% csrf_token %}
12+                    <div class="mb-3 row">
13+                        <label for="id_username" class="col-sm-4 col-form-label">ユーザー名</label>
14+                        <div class="col-sm-8">
15+                            {{ form.username }}
16+                        </div>
17+                    </div>
18+                    <div class="mb-3 row">
19+                        <label for="id_password" class="col-sm-4 col-form-label">パスワード</label>
20+                        <div class="col-sm-8">
21+                            {{ form.password }}
22+                        </div>
23+                    </div>
24+                    <div class="mb-3 row">
25+                        <div class="col-sm-8 offset-sm-4">
26+                            <button type="submit" class="btn btn-primary">ログイン</button>
27+                        </div>
28+                    </div>
29+                </form>
30+            </div>
31+        </div>
32+    </section>
33+{% endblock %}
34

/templates/registration/signup.html

/templates/registration/signup.html
1+{% extends 'base.html' %}
2+
3+{% block title %}ユーザー登録画面{% endblock %}
4+
5+{% block content %}
6+    <section class="container">
7+        <div class="row justify-content-center">
8+            <div class="col-md-6">
9+                <h2 class="mb-4">ユーザー登録画面</h2>
10+                <form method="post">
11+                    {% csrf_token %}
12+                    <div class="mb-3 row">
13+                        <label for="id_username" class="col-sm-4 col-form-label">ユーザー名</label>
14+                        <div class="col-sm-8">
15+                            {{ form.username }}
16+                        </div>
17+                    </div>
18+                    <div class="mb-3 row">
19+                        <label for="id_email" class="col-sm-4 col-form-label">メールアドレス</label>
20+                        <div class="col-sm-8">
21+                            {{ form.email }}
22+                        </div>
23+                    </div>
24+                    <div class="mb-3 row">
25+                        <label for="id_password1" class="col-sm-4 col-form-label">パスワード</label>
26+                        <div class="col-sm-8">
27+                            {{ form.password1 }}
28+                        </div>
29+                    </div>
30+                    <div class="mb-3 row">
31+                        <label for="id_password2" class="col-sm-4 col-form-label">パスワード(確認用)</label>
32+                        <div class="col-sm-8">
33+                            {{ form.password2 }}
34+                        </div>
35+                    </div>
36+                    <div class="mb-3 row">
37+                        <div class="col-sm-8 offset-sm-4">
38+                            <button type="submit" class="btn btn-primary">登録</button>
39+                        </div>
40+                    </div>
41+                </form>
42+            </div>
43+        </div>
44+    </section>
45+{% endblock %}
46

コード解説

変更点: サインアップ(ユーザー登録)フォームの作成

/app/forms.py
1 # app/forms.py
2 from django import forms
3 from .models import Board
4+from django.contrib.auth.forms import UserCreationForm
5+from django.contrib.auth.models import User
6 
7 class BoardForm(forms.ModelForm):
8     class Meta:
9         model = Board
10         fields = ['title', 'content']
11+
12+class SignUpForm(UserCreationForm):
13+    email = forms.EmailField(max_length=254, help_text='Required. Inform a valid email address.')
14+
15+    class Meta:
16+        model = User
17+        fields = ['username', 'email', 'password1', 'password2']
18

ユーザー登録(サインアップ)機能を実現するためのフォームを定義しています。Djangoが標準で提供しているUserCreationFormを継承することで、ユーザー名やパスワードの入力チェックといった複雑な処理を自分で書くことなく、安全なユーザー登録フォームを簡単に作成できます。 ここでは、標準のユーザー名とパスワードに加えて、メールアドレス(email)も登録できるようにフィールドを追加しています。Metaクラスでは、このフォームがどのモデル(Userモデル)と連携し、どのフィールド(username, email, password1, password2)を表示するかを指定しています。

変更点: 掲示板機能へのアクセス制限

/app/urls.py
1 # app/urls.py
2 from django.urls import path
3+from django.contrib.auth import views as auth_views
4+from django.contrib.auth.decorators import login_required
5 from . import views
6 
7 urlpatterns = [
8     path('', views.index, name='index'),
9-    path('new/', views.new, name='new'),
10-    path('create/', views.create, name='create'),
11-    path('<int:pk>/', views.show, name='show'),
12-    path('<int:pk>/edit/', views.edit, name='edit'),
13-    path('<int:pk>/update/', views.update, name='update'),
14-    path('<int:pk>/delete/', views.delete, name='delete'),
15+    path('new/', login_required(views.new), name='new'),
16+    path('create/', login_required(views.create), name='create'),
17+    path('<int:pk>/', login_required(views.show), name='show'),
18+    path('<int:pk>/edit/', login_required(views.edit), name='edit'),
19+    path('<int:pk>/update/', login_required(views.update), name='update'),
20+    path('<int:pk>/delete/', login_required(views.delete), name='delete'),
21 ]

ログインしていないユーザーが掲示板の各機能(新規作成、詳細表示、編集、削除など)にアクセスできないように制限をかけています。 login_requiredはDjangoが提供する便利な機能で、これをビュー関数に適用すると、そのページにアクセスする前にログインしているかどうかを自動でチェックしてくれます。もしログインしていなければ、設定されたログインページに自動的にリダイレクト(転送)されます。これにより、会員限定のページを簡単に実現できます。

変更点: ビューへのアクセス制限と認証関連ビューの追加

/app/views.py
1 # app/views.py
2 from django.shortcuts import render, redirect, get_object_or_404
3 from .models import Board
4-from .forms import BoardForm
5+from .forms import BoardForm, SignUpForm
6+from django.contrib.auth import logout
7+from django.contrib.auth.views import LoginView
8+from django.contrib.auth.decorators import login_required
9...
10+@login_required
11 def new(request):
12     form = BoardForm()
13     return render(request, 'new.html', {'form': form})
14 
15+@login_required
16 def create(request):
17...
18+@login_required
19 def show(request, pk):
20...
21+@login_required
22 def edit(request, pk):
23...
24+@login_required
25 def update(request, pk):
26...
27+@login_required
28 def delete(request, pk):
29...
30+
31+# ログインページのビュー
32+class CustomLoginView(LoginView):
33+    template_name = 'registration/login.html'
34+
35+# ログアウト
36+def logout_view(request):
37+    logout(request)
38+    return redirect('index')
39+
40+# サインアップページのビュー
41+def signup(request):
42+    if request.method == 'POST':
43+        form = SignUpForm(request.POST)
44+        if form.is_valid():
45+            form.save()
46+            return redirect('login')
47+    else:
48+        form = SignUpForm()
49+    return render(request, 'registration/signup.html', {'form': form})
50+
51+# プロフィールページのビュー
52+@login_required
53+def profile(request):
54+    user = request.user
55+    context = {
56+        'user': user
57+    }
58+    return render(request, 'accounts/profile.html', context)

まず、urls.pyでの設定と同様に、各ビュー関数の直前に@login_requiredデコレータを追加しています。これにより、関数が実行される前にログイン状態がチェックされます。 次に、ユーザー認証に必要な3つのビュー(ログイン、ログアウト、サインアップ)と、プロフィール表示用のビューを追加しています。

  • CustomLoginView: Django標準のLoginViewを継承し、ログイン画面で表示するHTMLテンプレートファイルをtemplate_nameで指定しています。
  • logout_view: Djangoのlogout関数を呼び出すだけで、簡単にログアウト処理ができます。処理後はトップページ(index)にリダイレクトしています。
  • signup: ユーザー登録処理を行います。POSTメソッド(フォームが送信された時)の場合、入力内容を検証し、問題がなければform.save()でユーザー情報をデータベースに保存します。登録成功後はログインページにリダイレクトします。GETメソッド(最初にページを開いた時)の場合は、空のフォームを表示します。
  • profile: request.userと書くことで、現在ログインしているユーザーの情報を取得できます。これをテンプレートに渡して、プロフィール情報を表示します。

変更点: プロジェクト全体のURL設定

/config/urls.py
1 """
2 from django.contrib import admin
3 from django.urls import path, include
4+from django.contrib.auth import views as auth_views
5+from app import views
6 
7 urlpatterns = [
8     path("admin/", admin.site.urls),
9     path('', include('app.urls')),
10+    path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
11+    path('accounts/logout/', views.logout_view, name='logout'),
12+    path('accounts/signup/', views.signup, name='signup'),
13+    path('accounts/profile/', views.profile, name='profile'),
14 ]

プロジェクト全体のURLとビューを関連付けるファイルです。ここでは、/accounts/から始まるURLパターンを新たに追加しています。

  • /accounts/login/にアクセスがあった場合は、Djangoが提供する汎用的なログインビューauth_views.LoginViewを呼び出します。
  • /accounts/logout/にアクセスがあった場合は、先ほどapp/views.pyで作成したlogout_view関数を呼び出します。
  • /accounts/signup//accounts/profile/も同様に、app/views.pyで作成したsignup関数とprofile関数をそれぞれ呼び出します。 name='login'のように各URLに名前を付けておくことで、テンプレートファイルなどから{% url 'login' %}のように簡単にURLを呼び出すことができます。

変更点: プロフィールページのテンプレート作成

/templates/accounts/profile.html
1+{% extends 'base.html' %}
2+
3+{% block title %}プロフィール画面{% endblock %}
4+
5+{% block content %}
6+    <section class="container">
7+        <div class="row justify-content-center">
8+            <div class="col-md-6">
9+                <h2 class="mb-4">プロフィール画面</h2>
10+                <p>ユーザー名: {{ user.username }}</p>
11+                <p>メールアドレス: {{ user.email }}</p>
12+            </div>
13+        </div>
14+        <a href="{% url 'index' %}" class="btn btn-primary">一覧に戻る</a>
15+    </section>
16+{% endblock %}

ログインしたユーザーのプロフィール情報を表示するためのHTMLテンプレートです。 {% extends 'base.html' %}は、サイト全体の共通レイアウトが定義されたbase.htmlを継承することを意味します。 ビュー関数から渡されたuserオブジェクトの情報を、{{ user.username }}{{ user.email }}のように波括弧二つで囲んで記述することで、ページ上にユーザー名やメールアドレスを表示させています。

変更点: ログイン状態でヘッダー表示を切り替える

/templates/base.html
1     <!-- ヘッダーの内容 -->
2     <header class="bg-success">
3         <div class="container">
4-            <div class="pt-3 pb-1 mb-2 text-white">
5+            <div class="pt-3 pb-1 mb-2 text-white d-flex justify-content-between align-items-center flex-wrap">
6                 <h1 class="display-4">掲示板アプリ</h1>
7+                {% if user.is_authenticated %}
8+                    <!-- ログインしている場合 -->
9+                    <div>
10+                        <p class="text-light">ようこそ {{ user.username }} さん</p>
11+                    </div>
12+                    <div>
13+                        <form action="{% url 'logout' %}" method="post" style="display: inline;">
14+                            {% csrf_token %}
15+                            <button type="submit" class="btn btn-light">ログアウト</button>
16+                        </form>
17+                    </div>
18+                    {% else %}
19+                    <!-- ログインしていない場合 -->
20+                    <div>
21+                        <a href="{% url 'signup' %}" class="btn btn-light">サインアップ</a>
22+                        <a href="{% url 'login' %}" class="btn btn-light">ログイン</a>
23+                    </div>
24+                {% endif %}
25+                </div>
26             </div>
27         </div>
28     </header>

全ページで共通して使われるbase.htmlのヘッダー部分を変更し、ユーザーのログイン状態に応じて表示内容を動的に切り替えています。 {% if user.is_authenticated %}というテンプレートタグを使うことで、ユーザーがログインしているかどうかを判定できます。

  • ログインしている場合 (True の場合) は、「ようこそ ○○ さん」というメッセージとログアウトボタンを表示します。ログアウトはPOSTリクエストで行うため、<form>タグで囲んでいます。{% csrf_token %}はセキュリティ対策のために必須の記述です。
  • ログインしていない場合 (False の場合) は、{% else %}以降の処理が実行され、サインアップボタンとログインボタンを表示します。

変更点: ログインページのテンプレート作成

/templates/registration/login.html
1+{% extends 'base.html' %}
2+
3+{% block title %}ログイン画面{% endblock %}
4+
5+{% block content %}
6+    <section class="container">
7+        <div class="row justify-content-center">
8+            <div class="col-md-6">
9+                <h2 class="mb-4">ログイン画面</h2>
10+                <form method="post">
11+                    {% csrf_token %}
12+                    <div class="mb-3 row">
13+                        <label for="id_username" class="col-sm-4 col-form-label">ユーザー名</label>
14+                        <div class="col-sm-8">
15+                            {{ form.username }}
16+                        </div>
17+                    </div>
18+                    <div class="mb-3 row">
19+                        <label for="id_password" class="col-sm-4 col-form-label">パスワード</label>
20+                        <div class="col-sm-8">
21+                            {{ form.password }}
22+                        </div>
23+                    </div>
24+                    <div class="mb-3 row">
25+                        <div class="col-sm-8 offset-sm-4">
26+                            <button type="submit" class="btn btn-primary">ログイン</button>
27+                        </div>
28+                    </div>
29+                </form>
30+            </div>
31+        </div>
32+    </section>
33+{% endblock %}

ログイン機能のためのHTMLテンプレートです。views.pyで指定したCustomLoginViewで使われます。 method="post"を指定した<form>タグの中に、ログインに必要な入力項目を配置しています。{% csrf_token %}はセキュリティ上のおまじないとして必須です。 {{ form.username }}{{ form.password }}と記述するだけで、Djangoが自動的に適切なHTMLの入力欄(<input>タグ)を生成してくれます。これにより、HTMLを手で書く手間が省け、コードがシンプルになります。

変更点: サインアップページのテンプレート作成

/templates/registration/signup.html
1+{% extends 'base.html' %}
2+
3+{% block title %}ユーザー登録画面{% endblock %}
4+
5+{% block content %}
6+    <section class="container">
7+        <div class="row justify-content-center">
8+            <div class="col-md-6">
9+                <h2 class="mb-4">ユーザー登録画面</h2>
10+                <form method="post">
11+                    {% csrf_token %}
12+                    <div class="mb-3 row">
13+                        <label for="id_username" class="col-sm-4 col-form-label">ユーザー名</label>
14+                        <div class="col-sm-8">
15+                            {{ form.username }}
16+                        </div>
17+                    </div>
18+                    <div class="mb-3 row">
19+                        <label for="id_email" class="col-sm-4 col-form-label">メールアドレス</label>
20+                        <div class="col-sm-8">
21+                            {{ form.email }}
22+                        </div>
23+                    </div>
24+                    <div class="mb-3 row">
25+                        <label for="id_password1" class="col-sm-4 col-form-label">パスワード</label>
26+                        <div class="col-sm-8">
27+                            {{ form.password1 }}
28+                        </div>
29+                    </div>
30+                    <div class="mb-3 row">
31+                        <label for="id_password2" class="col-sm-4 col-form-label">パスワード(確認用)</label>
32+                        <div class="col-sm-8">
33+                            {{ form.password2 }}
34+                        </div>
35+                    </div>
36+                    <div class="mb-3 row">
37+                        <div class="col-sm-8 offset-sm-4">
38+                            <button type="submit" class="btn btn-primary">登録</button>
39+                        </div>
40+                    </div>
41+                </form>
42+            </div>
43+        </div>
44+    </section>
45+{% endblock %}

ユーザー登録(サインアップ)機能のためのHTMLテンプレートです。views.pysignup関数で使われます。 基本的な構造はログインページと同じですが、forms.pyで定義したSignUpFormに合わせて、ユーザー名、メールアドレス、パスワード、パスワード(確認用)の4つの入力欄を{{ form.username }}のように配置しています。ユーザーがこのフォームに入力して「登録」ボタンを押すと、入力されたデータがサーバーに送信され、signupビューで処理されます。

おわりに

おわりに 今回はDjangoの強力なユーザー認証システムを使い、サインアップ・ログイン・ログアウト機能を実装しました。ビューに@login_requiredデコレータを付けるだけで簡単にアクセス制限をかけられることや、テンプレートでuser.is_authenticatedを使いログイン状態に応じて表示を切り替える方法を学びました。Djangoが提供するUserCreationFormLoginViewなどを活用することで、少ないコードで安全かつ本格的な認証機能が実現できます。この記事で作成した掲示板アプリをベースに、さらに機能を追加してオリジナルのWebアプリケーション開発に挑戦してみてください。

関連コンテンツ