今回は複数の権限(スーパーユーザー、スタッフユーザー、アクティブユーザー)のユーザーが存在するサイトにおいて、Djangoの認証機能を使用して特定のビューへのアクセスを制限するといったことを行っていきます。
例えば下図のようなブログサイトがあるとしたら
もちろんスーパーユーザーは全てのビューへアクセス可能ですが、「スタッフユーザー」は「非公開リストまで、「アクティブユーザー」は記事詳細までといった内容でログインされてきたユーザーに対してそれぞれの異なる認証を与えます。
今回はパーミッション(許可)などの細かい設定は行いませんが、細かい設定を行えるようになるためにも代表的な認証機能を使ってみたいと思います。
Djangoの認証機能は、デコレータやクラスベースビューとして簡単に設定することができるのでぜひ試してみましょう。
実行環境 |
---|
Windows Subsystem for Linux Ubuntu |
Python 3.6.9 |
pip 9.0.1 |
使用ライブラリ | ライセンス |
---|---|
Django==3.1 | BSD |
アクセス制限をするにあたって何を制限とするか?
それはユーザーの権限に従った制限です。
権限には3つの種類があり、上から「スーパーユーザー」「スタッフユーザー」「アクティブユーザー」となります。
チェンジリストからでは分かり兼ねる部分があるかと思いますが、それぞれ
ユーザー名 | 権限 |
---|---|
admin | スーパーユーザー |
django | スタッフユーザー |
python | アクティブユーザー |
として保存されています。
パーミッションなどは設定していないので、スーパーユーザー以外は管理画面を扱う事が許されていません。
※自由に設定可能です
実装するにあたりログイン中のユーザーをしっかり把握するために、ベースとなるHTMLファイルに組込みタグを使用して各権限の持ったユーザー名を出力しておきます。
<!-- app/templates/app/base.html -->
<!doctype html>
<html>
<head>
<title>ブログ</title>
</head>
<body>
<div class="container">
<!-- ログイン中のユーザーを表示 -->
{% if request.user.is_authenticated %}
<h1>
ブログ {{ user.username }}さんは
{% if request.user.is_superuser %}
スーパーユーザー
{% elif request.user.is_staff %}
スタッフユーザー
{% elif request.user.is_active %}
アクティブユーザー
{% endif %}
です
</h1>
{% else %}
<h1>ブログ 一般ユーザーさんです</h1>
{% endif %}
<br>
<!-- ここまで -->
{% block content %}
{% endblock %}
</body>
</html>
下図のようにログイン中のユーザーによって「ユーザー名」「権限」が表示されます。
ログインがされていない状況で、アクセス制限がされているページにアクセスされた時に、ログインをさせるか、それとも権限の無いユーザーには違うページにアクセスさせるか設定できますが、アクセス制限に使う認証機能とセットでログイン機能も使われることがあるので、準備をしておきます。
認証機能のモジュールとして「'django.contrib.auth'」が設定ファイル内のINSTALLED_APPSに定義されているので、インポートをすることによって様々な機能が使えます。
では、アプリディレクトリ内の「urls.py」にログイン機能のビューを定義します。
# app/urls.py
from django.urls import path
from . import views
from django.contrib.auth import views as auth_views # インポート
app_name = 'app'
urlpatterns = [
...
path('/accounts/login/', auth_views.LoginView.as_view()), # 追加
...
...
]
URLパターンを「/accounts/login/」としているのは、認証機能を使う際にデフォルトで設定されているログインページへのURLだからです。
もちろん好きなURLパターンに変えることができますが、少し手間を省くために予め設定しています。
LoginViewクラスのデフォルト設定では
属性 | デフォルト |
---|---|
name | login |
template_name | registration/ login.html |
form | AuthenticationForm |
などです。
他にもありますが、今回は簡単なログイン機能の実装なので詳しくはドキュメントをご参照ください。
ではデフォルトで設定されているテンプレート先の「registration/login.html」を作成します。
templates
|--registration
|--login.html
# app/templates/registration/login.html
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="ログイン">
</form>
LoginViewクラスのフォームには「 AuthenticationForm」というフォームクラスがデフォルトで設定されているので、オブジェクトの名前は「form」として取得できます。
ブラウザの検索バーに「http://127.0.0.1:8000/accounts/login/?next=/」と打ち込むと、以下のようにログインフォームが表示されるはずです。
URL末尾の「?next=/」となっているのは、ログインされた際にトップページである「/」にリダイレクトされるという意味です。
恐らく頻繁にログアウトを実行するかと思うので、少し面倒を省くために管理画面からログアウトする際のリダイレクト先をトップページに設定します。
設定ファイル「settings.py」の最下部にLOGOUT_REDIRECT_URLを設置します。
# config/settings.py
LOGOUT_REDIRECT_URL = '/' # トップページへリダイレクトされる
まずは単純な方法として、「request」オブジェクトを使用した権限による処理を実装します。
ビューの処理において定義された関数ビューやクラスベースビュー内で、この「request」オブジェクトのUser属性を取得し('request.user'など)、取得したUser属性からさらにユーザーの情報を取得し('request.user.is_superuser'など)条件分岐による各権限の処理を行います。
ここでは最も権限の低い「アクティブユーザー」は、「詳細ページ」のみのアクセスを許し、「スタッフユーザー」は「詳細ページ」と「非公開リスト」のアクセスを許します。
もちろんスーパーユーザーはすべての権限を有しているので、「投稿」ページのみスーパーユーザーの権限を設定します。
では関数ビューとクラスベースビューでそれぞれ確認してみます。
# app/views.py
from django.shortcuts import render, get_object_or_404, redirect
from .models import Blog
from .forms import BlogForm
...
def blog_private(request):
"""
非公開リスト
"""
# スタッフユーザーの権限がなければログインページ
if not request.user.is_staff:
return redirect('/accounts/login/?next=%s' % request.path)
blog_private = Blog.objects.filter(is_publick=False).order_by('-id')
return render(request, 'app/blog_private.html', {'blog_private': blog_private})
def detail(request, pk):
"""
記事詳細
"""
# アクティブユーザーの権限がなければログインページ
if not request.user.is_active:
return redirect('/accounts/login/?next=%s' % request.path)
blog_detail = get_object_or_404(Blog, id=pk, is_publick=True)
context = {
'blog_detail': blog_detail,
}
return render(request, 'app/detail.html', context)
def new_blog(request):
"""
投稿
"""
# スーパーユーザーの権限がなければログインページ
if not request.user.is_superuser:
return redirect('/accounts/login/?next=%s' % request.path)
if request.method =="POST":
form = BlogForm(request.POST)
if form.is_valid():
form.save()
return redirect('app:index')
else:
form =BlogForm
return render(request, 'app/new_blog.html', {'form': form})
制限したい各アクセスポイントに「request.user...」の条件を設定するだけで、サイト訪問状態の権限によってログインページもしくわそのページにアクセスされます。
pythonユーザーは記事詳細ページにアクセスする権限があるので、URLの「/?next=/detail/id/」とされるページにリダイレクトされる
# app/views.py
from django.shortcuts import render, redirect
from .models import Blog
from .forms import BlogForm
from django.views.generic import ListView, FormView, DetailView
...
class Blog_Private(ListView):
"""
非公開リスト
"""
queryset = Blog.objects.filter(is_publick=False).order_by('-id')
template_name = 'app/blog_private.html'
context_object_name = 'blog_private'
def get(self, request):
# スタッフユーザーでなければログインページ
if not request.user.is_staff:
return redirect('/accounts/login/?next=%s' % request.path)
return super().get(request)
class Detail(DetailView):
"""
記事詳細ページ
"""
model = Blog
template_name = 'app/detail.html'
context_object_name = 'blog_detail'
def get(self, request, **kwargs):
# アクティブユーザーでなければログインページ
if not request.user.is_active:
return redirect('/accounts/login/?next=%s' % request.path)
return super().get(request)
class New_Blog(FormView):
"""
投稿
"""
form_class = BlogForm
template_name = 'app/new_blog.html'
success_url = '/'
def get(self, request):
# スーパーユーザーでなければログインページ
if not self.request.user.is_superuser:
return redirect('/accounts/login/?next=%s' % request.path)
return super().get(request)
def form_valid(self, form):
form.save()
return super().form_valid(form)
制限したい各アクセスポイントに「request.user...」の条件を設定するだけで、サイト訪問状態の権限によってログインページもしくわそのページにアクセスされます。
先ほどのビューでWebリクエストを行ったような権限による認証のテストをショートカット機能として実装することができます。
関数ビューである場合は「user_passes_test」デコレータを使用し、クラスベースビューでの場合は「UserPassesTestMixin」クラスをサブクラス化して使用します。
ユーザーに対するテストは自由に決めることができますが、ここでは権限に対してのテストを実行させテストにパスできなければアクセスできないようにします。
このデコレータは「user_passes_terst(test_func, login_url=None、redirect_field_name='next')」という3つの引数を取れます。
※簡単なログイン機能を作成した際に、URLパターンを「/accounts/login/」とした理由はこれらの認証機能のデフォルトがそこにアクセスされるからです。
# app/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import user_passes_test # インポート
from .models import Blog
from .forms import BlogForm
...
def staff_test_func(user):
return user.is_staff
@user_passes_test(staff_test_func) # ログイン時スタッフユーザーであるかテスト
def blog_private(request):
"""
非公開リスト
"""
blog_private = Blog.objects.filter(is_publick=False).order_by('-id')
return render(request, 'app/blog_private.html', {'blog_private': blog_private})
def active_test_func(user):
return user.is_active
@user_passes_test(active_test_func) # ログイン時アクティブユーザーであるかテスト
def detail(request, pk):
"""
記事詳細ページ
"""
blog_detail = get_object_or_404(Blog, id=pk, is_publick=True)
context = {
'blog_detail': blog_detail,
}
return render(request, 'app/detail.html', context)
def super_test_func(user):
return user.is_superuser
@user_passes_test(super_test_func) # ログイン時スーパーユーザーであるかテスト
def new_blog(request):
"""
投稿
"""
if request.method =="POST":
form = BlogForm(request.POST)
if form.is_valid():
form.save()
return redirect('app:index')
else:
form =BlogForm
return render(request, 'app/new_blog.html', {'form': form})
これでWebリクエストで行った権限に従うアクセスと同じような挙動になるかと思います。
先ほどデフォルトでログインページにアクセスされると言いましたが、URLパターンで定義している「/accounts/login/」行をコメントアウトしてみると
# app/urls.py
urlpatterns = [
...
# path('accounts/login/', auth_views.LoginView.as_view()),
...
]
以下のようにエラーとなります。
引数の「redirect_field_name」を指定して、リダイレクト先をユーザープロフィールのあるビューへ渡してみます。
まずはアプリディレクトリ内の「urls.py」を編集します。
# app/urls.py
urlpatterns = [
...
path('accounts/login/', auth_views.LoginView.as_view()),
...
path('accounts/profile/', views.account_profile, name='account_profile'), # 追記
]
「views.py」にて簡単なプロフィールビューを定義します。
# app/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import user_passes_test
from .models import Blog
from .forms import BlogForm
from django.http import HttpResponse # インポート
...
@user_passes_test(staff_test_func, redirect_field_name='account_profile')
def blog_private(request):
"""
非公開リスト
"""
...
@user_passes_test(active_test_func, redirect_field_name='account_profile')
def detail(request, pk):
"""
記事詳細ページ
"""
...
@user_passes_test(super_test_func, redirect_field_name='account_profile')
def new_blog(request):
"""
投稿
"""
...
def account_profile(request):
"""
プロフィールページ
"""
return HttpResponse('<h1>ようこそ%sさん' % request.user.username)
認証テストに合格したユーザーはプロフィールページへリダイレクトされ、アクティブである以上特定のページへアクセスできますが、認証されない場合は、永遠とログイン→プロフィール画面の繰り返しとなります。
UserPassesTestMixinクラスは、AccessMixinクラスを継承しているので設定次第では様々な処理を行えます。
AccessMixinはユーザーのログインでテストに失敗した際の動作を変えることができます。
代表的な属性は
クラスメソッド
属性とクラスメソッドの呼び出し
# 例
from django.contrib.auth.mixins import UserPassesTestMixin
from .models import Blog
from django.views.generic import ListView
class View(UserPassesTestMixin, ListView):
model = Blog
template_name = 'app/index'
# 認証機能
login_url ='/account/login/'
redirect_filed_name = 'profile'
permission_denied_message = 'テストに失敗しました。'
raise_exception = True
def test_func(self):
# Gメールが登録されているユーザーはアクセス可能とする
return self.request.user.email.endswith('@gmail.com')
他にもオーバーライドできるメソッドが揃っているので、興味のある方はドキュメントをご参照ください。
ここではデフォルトの状態で実装します。
# app/views.py
from django.contrib.auth.mixins import UserPassesTestMixin # インポート
from .models import Blog
from .forms import BlogForm
from django.views.generic import ListView, FormView, DetailView
...
class Blog_Private(UserPassesTestMixin, ListView):
"""
非公開リスト
ユーザー認証機能継承
"""
queryset = Blog.objects.filter(is_publick=False).order_by('-id')
template_name = 'app/blog_private.html'
context_object_name = 'blog_private'
def test_func(self):
# スタッフユーザーでなければ403ページ
return self.request.user.is_staff
class Detail(UserPassesTestMixin, DetailView):
"""
記事詳細ページ
ユーザー認証機能継承
"""
model = Blog
template_name = 'app/detail.html'
context_object_name = 'blog_detail'
def test_func(self):
# アクティブユーザーでなければ403ページ
return self.request.user.is_active
class New_Blog(UserPassesTestMixin, FormView):
"""
投稿
ユーザー認証機能継承
"""
form_class = BlogForm
template_name = 'app/new_blog.html'
success_url = '/'
def test_func(self):
# スーパーユーザーでなければ403ページ
return self.request.user.is_superuser
def form_valid(self, form):
form.save()
return super().form_valid(form)
これでWebリクエストで行った権限に従うアクセスと同じような挙動になるかと思います。
ただし、AccessMixinクラスを継承しているので、テスト後の動作が違います。
ログイン中のユーザーの権限が合わなければ、常に「403 Forbidden」を表示します。
ログインページにアクセスさせるシンプルなショートカット機能として、login_requiredがあります。
単体で使うと権限やパーミッションなどの細かい設定はできませんが、他のショートカット機能と組み合わせて使用することができます。
関数ビューである場合は「login_required」デコレータを使用し、クラスベースビューでの場合は「LoginRequiredMixin」クラスをサブクラス化して使用します。
このデコレータは「login_required(login_url=None、redirect_field_name='next')」という2つの引数を取れます。
※簡単なログイン機能を作成した際に、URLパターンを「/accounts/login/」とした理由はこれらの認証機能のデフォルトがそこにアクセスされるからです。
# app/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required # インポート
from .models import Blog
from .forms import BlogForm
from django.http import HttpResponse # インポート
...
@login_required
def blog_private(request):
"""
非公開リスト
"""
if not request.user.is_staff:
return HttpResponse('<h1>%sさんはアクセスできません</h1>' % request.user.username)
blog_private = Blog.objects.filter(is_publick=False).order_by('-id')
return render(request, 'app/blog_private.html', {'blog_private': blog_private})
@login_required
def detail(request, pk):
"""
記事詳細ページ
"""
if not request.user.is_active:
return HttpResponse('<h1>%sさんはアクセスできません</h1>' % request.user.username)
blog_detail = get_object_or_404(Blog, id=pk, is_publick=True)
context = {
'blog_detail': blog_detail,
}
return render(request, 'app/detail.html', context)
@login_required
def new_blog(request):
"""
投稿
"""
if not request.user.is_superuser:
return HttpResponse('<h1>%sさんはアクセスできません</h1>' % request.user.username)
if request.method =="POST":
form = BlogForm(request.POST)
if form.is_valid():
form.save()
return redirect('app:index')
else:
form =BlogForm
return render(request, 'app/new_blog.html', {'form': form})
シンプルなショートカット機能を使用する際は、各権限による処理は自分で定義します。
スタッフユーザーの権限しかアクセスできない「非公開リスト」(blog_private)にアクティブユーザーであるpythonユーザーでログインすると
LoginRequiredMixinクラスは、AccessMixinクラスを継承しているので、UserPassesTestMixinと同様設定次第では様々な処理を行えます。
代表的な属性は
属性の呼び出し
# 例
from django.contrib.auth.mixins import UserPassesTestMixin
from .models import Blog
from django.views.generic import ListView
class View(UserPassesTestMixin, ListView):
model = Blog
template_name = 'app/index'
# 認証機能
login_url ='/account/login/'
redirect_filed_name = 'profile'
permission_denied_message = 'テストに失敗しました。'
raise_exception = True
他にもオーバーライドできるメソッドが揃っているので、興味のある方はドキュメントをご参照ください。
ここではデフォルトの状態で実装します。
# app/views.py
from django.contrib.auth.mixins import LoginRequiredMixin # インポート
from .models import Blog
from .forms import BlogForm
from django.http import HttpResponse # インポート
...
class Blog_Private(LoginRequiredMixin, ListView):
"""
非公開リスト
認証機能継承
"""
queryset = Blog.objects.filter(is_publick=False).order_by('-id')
template_name = 'app/blog_private.html'
context_object_name = 'blog_private'
def get(self, request):
# スタッフユーザーでなければHTMLを返す
if not request.user.is_staff:
return HttpResponse('<h1>%sさんはアクセスできません</h1>' % request.user.username)
return super().get(request)
class Detail(LoginRequiredMixin, DetailView):
"""
記事詳細ページ
認証機能継承
"""
model = Blog
template_name = 'app/detail.html'
context_object_name = 'blog_detail'
def get(self, request, **kwags):
# アクティブユーザーでなければHTMLを返す
if not request.user.is_active:
return HttpResponse('<h1>%sさんはアクセスできません</h1>' % request.user.username)
return super().get(request)
class New_Blog(LoginRequiredMixin, FormView):
"""
投稿
認証機能継承
"""
form_class = BlogForm
template_name = 'app/new_blog.html'
success_url = '/'
def get(self, request):
# スーパーユーザーでなければHTMLを返す
if not request.user.is_superuser:
return HttpResponse('<h1>%sさんはアクセスできません</h1>' % request.user.username)
return super().get(request)
def form_valid(self, form):
form.save()
return super().form_valid(form)
AccessMixinクラスを継承しているので、メソッドをオーバーライドすることで権限のテストなどを行うことができそうですが、ここでは権限のテストを自分で定義しました。
今回は権限による認証機能をいくつか試してみましたが、認証機能の種類、共に内容もまだまだ説明しきれていません。
余力のある方はどんどん試して行きましょう。
それでは以上となります。
最後までご覧いただきありがとうございました。