Django中級 #4 ユーザー / 権限 — カスタム user model、permission、group
基礎 #7 で Django の ビルトイン認証 を固めました — ログイン/ログアウト、@login_required、User モデル。実務に入るとすぐに 2 つの問いに出会います。
- 「username の代わりに メール でログインしたいんだけど?」
- 「ユーザーに細かな権限を与えたいなら?」
この記事がその答えです。重い警告 1 つから始めます — AUTH_USER_MODEL はプロジェクト開始時点で決めなければなりません。
重い警告 — 開始時点で決めるべき決定 #
Django 公式ドキュメントが強調する一文: “Even if you’re happy with the default User model, you’ll likely want to change it later. So if you’re starting a new project, definitely set up a custom user model, even if it looks just like Django’s default user model.”
(訳: デフォルトの User モデルで満足していても、後で変更したくなる可能性が高いです。新規プロジェクトなら、デフォルトの User と全く同じ見た目でも必ずカスタム user model で始めてください。)
理由は マイグレーション後に変更するのが非常に難しいため です。すべての外部キー、すべてのマイグレーション履歴、すべてのデータを移さなければなりません。新規プロジェクトでは次の 2 行を 最初のマイグレーション前に 押さえてください。
AUTH_USER_MODEL = "accounts.User"from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass今すぐ追加フィールドがなくても 自分の User クラスを持って始めます。後でフィールドを追加するのは簡単ですから。
AbstractUser vs AbstractBaseUser
#
カスタマイズの深さに応じて 2 つの分岐があります。
AbstractUser | AbstractBaseUser + PermissionsMixin | |
|---|---|---|
| 基本フィールド | username、email、first_name、last_name、… | password、last_login のみ |
| 権限システム | 自動含む | PermissionsMixin で追加 |
| 推奨されるケース | ほとんど (90%) — 少し追加するだけ | 全く違うユーザーモデル (メールのみなど) |
| 作業量 | 非常に少ない | 多い (UserManager を自前で書く) |
ほぼすべてのケースで AbstractUser が答え。 本当に username 自体をなくしてメールだけ使いたいときだけ AbstractBaseUser。
AbstractUser — フィールド追加だけ
#
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to="avatars/", blank=True, null=True)
birth_date = models.DateField(null=True, blank=True)
def __str__(self):
return self.usernameこれで終わりです。username、email、first_name、last_name、is_staff、is_active などはそのまま生きています。
メールログインパターン — AbstractBaseUser
#
実務でよく出会う要件 — メールを username として 使う。このときは AbstractBaseUser + カスタムマネージャの組み合わせが標準です。
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.db import models
from django.utils import timezone
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, password, **extra):
if not email:
raise ValueError("メールは必須です。")
email = self.normalize_email(email)
user = self.model(email=email, **extra)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra):
extra.setdefault("is_staff", False)
extra.setdefault("is_superuser", False)
return self._create_user(email, password, **extra)
def create_superuser(self, email, password=None, **extra):
extra.setdefault("is_staff", True)
extra.setdefault("is_superuser", True)
if extra.get("is_staff") is not True:
raise ValueError("superuser は is_staff=True でなければなりません。")
if extra.get("is_superuser") is not True:
raise ValueError("superuser は is_superuser=True でなければなりません。")
return self._create_user(email, password, **extra)
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
name = models.CharField(max_length=100, blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(default=timezone.now)
objects = UserManager()
USERNAME_FIELD = "email" # ログインに使うフィールド
REQUIRED_FIELDS = [] # createsuperuser が訊ねる追加フィールド (email/password 以外)
def __str__(self):
return self.emailチェックポイント:
USERNAME_FIELD = "email"— ログイン識別子。unique=True必須REQUIRED_FIELDS—createsuperuserコマンドが追加で訊ねるフィールド。USERNAME_FIELDとpasswordは自動で含む のでここに書かないUserManagerのcreate_user/create_superuserがmanage.py createsuperuserなどから呼ばれるPermissionsMixinを一緒に継承しなければグループ/パーミッションシステムが動かない
admin の登録 #
createsuperuser が動いても admin のユーザー変更フォームはデフォルトの User ベースなので壊れます。UserAdmin を直接登録します。
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import User
class UserAdmin(BaseUserAdmin):
ordering = ("email",)
list_display = ("email", "name", "is_staff", "is_active")
search_fields = ("email", "name")
fieldsets = (
(None, {"fields": ("email", "password")}),
("個人情報", {"fields": ("name",)}),
("権限", {"fields": ("is_active", "is_staff", "is_superuser",
"groups", "user_permissions")}),
("記録", {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(None, {"classes": ("wide",),
"fields": ("email", "password1", "password2")}),
)
admin.site.register(User, UserAdmin)会員登録/ログインフォームも username ベースのビルトインフォームをそのまま使えないので、UserCreationForm / AuthenticationForm を継承してメールベースで作り直します。
Permission システム #
Django はモデルごとに自動で 4 つのパーミッションを作ります。
| パーミッションコード | 意味 |
|---|---|
add_<model> | 追加 |
change_<model> | 修正 |
delete_<model> | 削除 |
view_<model> | 閲覧 |
blog アプリの Post モデルなら blog.add_post、blog.change_post、blog.delete_post、blog.view_post が自動生成されます。
コードでの検査 #
if request.user.has_perm("blog.change_post"):
# 修正権限あり
...
if request.user.has_perms(["blog.change_post", "blog.delete_post"]):
# 両方あり
...デコレータ / Mixin #
from django.contrib.auth.decorators import permission_required
@permission_required("blog.change_post", raise_exception=True)
def post_edit(request, pk):
...from django.contrib.auth.mixins import PermissionRequiredMixin
class PostUpdateView(PermissionRequiredMixin, UpdateView):
permission_required = "blog.change_post"
raise_exception = True#1 CBV で見たあの Mixin です。
カスタムパーミッション #
モデルの Meta に定義して自動生成に追加できます。
class Post(models.Model):
...
class Meta:
permissions = [
("publish_post", "Can publish post"),
("feature_post", "Can feature post on homepage"),
]makemigrations 後 blog.publish_post で使用。admin のユーザー/グループ編集で付与できます。
Group — パーミッションの束 #
権限をユーザーに直接与えると管理が難しくなります。Group にまとめてユーザーに付与する のが標準。
from django.contrib.auth.models import Group, Permission
editors, _ = Group.objects.get_or_create(name="エディター")
editors.permissions.add(
Permission.objects.get(codename="add_post"),
Permission.objects.get(codename="change_post"),
Permission.objects.get(codename="publish_post"),
)
# ユーザーにグループを付与
user.groups.add(editors)admin ページの グループ メニューでも同じことができます。普通はマイグレーションやシードコマンドでグループ/パーミッションの初期値をコードに埋め込むのが再現性の面で良いです。
from django.db import migrations
def create_default_groups(apps, schema_editor):
Group = apps.get_model("auth", "Group")
Permission = apps.get_model("auth", "Permission")
editors, _ = Group.objects.get_or_create(name="エディター")
perms = Permission.objects.filter(
codename__in=["add_post", "change_post", "publish_post"]
)
editors.permissions.set(perms)
class Migration(migrations.Migration):
dependencies = [("blog", "0001_initial")]
operations = [migrations.RunPython(create_default_groups, migrations.RunPython.noop)]テンプレートでのグループ/パーミッション検査 #
{% if user.is_authenticated %}
{% if perms.blog.publish_post %}
<a href="{% url 'post_publish' post.pk %}">公開</a>
{% endif %}
{% endif %}perms は django.contrib.auth.context_processors.auth コンテキストプロセッサが渡すヘルパーです。settings.py の TEMPLATES.OPTIONS.context_processors に入っていれば自動。
Object-level permission — 一行案内 #
Django のビルトインパーミッションは モデル単位 です — 「このユーザーが Post を修正できるか」までしかありません。「このユーザーがこの特定の Post を修正できるか」 (オブジェクト単位) はありません。
オブジェクト単位の権限が必要なら:
UserPassesTestMixin(#1) で直接検査 — 小さなケースなら十分- django-guardian 外部ライブラリ — 本格的なオブジェクト権限
- DRF の
permission_classes— REST API なら (DRF #2)
小さなケースでは直接検査がきれいです。
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
form_class = PostForm
def test_func(self):
return self.get_object().author == self.request.userパスワード — ハッシュ / 変更 / リセット #
Django はパスワードを PBKDF2 (デフォルト) などの安全なハッシュで自動保存します。直接触ることはほぼありません。
user.set_password("新しいパスワード") # ハッシュして保存
user.save()
user.check_password("入力値") # 検証平文パスワードを保存しないでください。user.password = "..." で直接代入すると平文が入るので 必ず set_password。
ビルトインのパスワード変更/リセットビューは django.contrib.auth.urls に入っています。
from django.urls import path, include
urlpatterns = [
path("accounts/", include("django.contrib.auth.urls")),
# /accounts/login/, /accounts/logout/, /accounts/password_change/,
# /accounts/password_reset/ など
]DRF の認証との違い — 短く #
今回の認証は セッション + クッキー (ブラウザフォームログイン) モデルです。REST API は普通 トークン / JWT / OAuth2 などの stateless 認証を使います。
| Django ビルトイン (今回) | DRF | |
|---|---|---|
| 認証媒体 | セッションクッキー | Token / JWT / OAuth2 |
| 権限検査 | has_perm / Mixin / デコレータ | permission_classes |
| ユーザーモデル | 同一 (AUTH_USER_MODEL) | 同一 |
| ログインフォーム | ビルトイン HTML | 別エンドポイント |
同じ User モデルを使いますが 認証媒体と権限検査の位置が異なる構造 と考えるとよいでしょう。DRF の認証/権限スタックは DRF #2 で詳しく。
request.user の正体
#
AuthenticationMiddleware (#3 で見たあのミドルウェア) がすべてのリクエストに request.user を埋めます。
- ログインしていれば →
Userインスタンス - していなければ →
AnonymousUser(偽物のオブジェクト。is_authenticated == False)
if request.user.is_authenticated:
...
if request.user.is_staff:
...
if request.user.is_superuser:
...is_authenticated は 属性 です (メソッドではありません)。request.user.is_authenticated() のように呼び出してはいけません — 古い Django 1.x 時代はメソッドでしたが今は属性。
まとめ #
今回押さえたもの:
AUTH_USER_MODELはプロジェクト開始時点で決める — 後の変更は非常に高くつく- ほぼすべてのケースで
AbstractUserが答え (フィールド追加だけ) - メールログイン —
AbstractBaseUser+PermissionsMixin+ カスタムUserManager USERNAME_FIELD、REQUIRED_FIELDSの意味- モデルごとに自動 4 つのパーミッション (
add/change/delete/view_<model>) has_perm、@permission_required、PermissionRequiredMixin- モデル
Meta.permissionsでカスタムパーミッション - Group で権限を束ねる — ユーザーにはグループを付与
- オブジェクト単位の権限は
UserPassesTestMixinまたはdjango-guardian/ DRF - パスワードは
set_password/check_password、ビルトイン変更/リセットビュー request.user.is_authenticatedは属性
次回 (#5 メッセージ/セッション/クッキー) では、認証されたユーザーに送る flash メッセージ、リクエスト間の状態保存のための セッション、そしてその下層の クッキー までを 1 編で解きます。