[Django] カスタムユーザーを利用する

Djangoのユーザー認証をDjango標準のUserモデルではなく自分で定義したものを利用するメモです。

カスタムユーザー

Djangoでカスタムユーザーを利用する場合は大きく以下の三つに分類できます。

  • データベースのリレーションを利用する
  • AbstractUserを継承する
  • AbstractBaseUserを継承する

一つ目はDjango標準のUserモデルにOneToOneFieldForeignKeyを利用してリレーションを持たせる方法です。
厳密にはカスタムユーザーではありませんが、お手軽に実装できます。

二つ目はAbstractUserを継承する方法です。
こちらは標準Userモデルが持っているフィールドは全て持っているので、フィールドを追加したい場合におススメです。

三つ目はAbstractBaseUserを継承する方法です。
こちらは最低限のフィールド、機能以外は持っていないのでフィールドを削りたい場合や動作を柔軟に定義したい場合におススメです。
ただし、柔軟に定義できる分記述する量も増えてきます。

メールアドレスでログインするユーザー

今回は、ログイン時、登録時にユーザー名ではなくメールアドレスを利用するケースを考えます。

AbstractUser

まずは、お手軽なAbstractUserを継承する方法から

実装

モデル

from django.contrib.auth.models import AbstractUser, UnicodeUsernameValidator
from django.db import models
from django.utils.translation import gettext_lazy as _

class User(AbstractUser):

    username_validator = UnicodeUsernameValidator()
    username = models.CharField(
        _('username'),
        max_length=150,
        help_text=_(
            'Required. 150 characters or fewer.'
            ' Letters, digits and @/./+/-/_ only.'),
        validators=[username_validator],
    )
    email = models.EmailField(
        _('email address'),
        unique=True,
        error_messages={
            'unique': _("A user with that email address already exists."),
        },
    )
    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    class Meta(AbstractUser.Meta):
        swappable = 'AUTH_USER_MODEL'

クラス変数のUSERNAME_FIELD 'email'にする事でログイン時に要求されるユーザー名をメールアドレスに変更しています。

    class Meta(AbstractUser.Meta):
        swappable = 'AUTH_USER_MODEL'

部分でDjango標準のユーザーと重複利用されるのを防いでいます。

AbstractBaseUser

次に、AbstractBaseUserを継承する方法を紹介します。

実装

モデル

from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin

class AbstractUser(AbstractBaseUser, PermissionsMixin):
    username_validator = UnicodeUsernameValidator()

    username = models.CharField(
        _('username'),
        max_length=150,
        help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
        validators=[username_validator],
        error_messages={
            'unique': _("A user with that username already exists."),
        },
        blank=True
    )
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=150, blank=True)
    email = models.EmailField(_('email address'), unique=True)
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_('Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects = UserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['email']

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')
        swappable = 'AUTH_USER_MODEL'

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def get_full_name(self):
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        send_mail(subject, message, from_email, [self.email], **kwargs)

こちらのAbstractBaseUserは、パーミッション関連の機能を持っていないので、パーミッションの機能を利用したい場合は、PermissionsMixinを同時に継承しておく必要があります。

Django標準のパーミッション機能は、実際にサービスを作る場合だと微妙に要件を満たさないことが多いので、あまり必要だと感じる機会はありませんが今回は一応継承しています。

マネージャー

from django.contrib.auth.base_user import BaseUserManager

class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, username, email, password, **extra_fields):
        if not email:
            raise ValueError('The given email must be set')
        email = self.normalize_email(email)
        username = self.model.normalize_username(username)
        user = self.model(username=username, email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, username, email=None, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(username, email, password, **extra_fields)

    def create_superuser(self, username, email=None, password=None, **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(username, email, password, **extra_fields)

create_superuserメソッドはpython manage.py createsuperuserコマンドの時に実行されます。

今回の要件の場合は、わざわざ面倒なAbstractBaseUserを継承する必要はありませんでしたが、登録時に色々な操作を挟む場合はこちらの方が有用な場合があります。

ログイン時に利用するユーザーを変更する

せっかく自分でカスタムユーザーを作成しても利用することを宣言しないとログイン時に利用してくれません。
なのでプロジェクトの設定に先ほど作成したカスタムユーザーモデルを利用するように設定を加えます。

AUTH_USER_MODEL = 'アプリケーション名.User'

アプリケーション名部分は、カスタムユーザーモデルを定義したアプリケーションの名前に置き換えてください。

あとは、適宜マイグレーションを実行してください。

まとめ

  • カスタムユーザーにフィールドを追加したい場合はAbstractUserを継承する
  • カスタムユーザーをより細かくカスタムするのならAbstractBaseUserを継承する
  • AUTH_USER_MODELの設定を忘れずに!