よしまさのウェブサイト

djangoのauth認証の入力項目を複数追加してみた。

ブログカテゴリー
システムやプログラムのメモ・雑感
投稿日時

djangoに標準搭載の認証機能authを使っているけど、すこぶる使い勝手が悪い。
通常状態だとusernameとpasswordで認証する仕組みになっているけど、これをemailとpasswordに変更するだけでも一苦労である。
今回はusernameとpasswordに加え、もう一つclient_idという項目を追加したログイン機能を作れないか試してみた。
要するに、アカウント名+パスワードだけではなく、会社IDを追加することでセキュリティを強化するというのが目標。
応用すれば、ログインの項目をusernameとpasswordだけではなく、これにグループIDやメールアドレスなどの3つ以上の項目を複数追加可能になる。
結構ニーズがあるのではなかろうかと思うけど、調べてみてどこにも方法が書かれていないので頑張ってみた。

1. 前提

ログイン用のアプリケーションの作成、および/accounts/login/でアクセス可能なルートが設定されていること。
以下のようにやればOK。

コマンドライン

$ python3 manage.py startapp accounts

/project/project/urls.py

urlpatterns = [
path('accounts/', include('accounts.urls')),
......
]

/project/accounts/urls.py

# project/accounts/urls.py

from django.urls import path
from . import views

app_name = 'accounts'

urlpatterns = [
    path('login/', views.Login.as_view(), name='login'),
    path('logout/', views.Logout.as_view(), name='logout'),
]

2. Userモデルをオーバーライド

初期のモデルのままだと、ログインに追加する項目が必須項目から抜けているので、これを必須にしておかないとログイン時にバグが発生する。
同時に、createsuperuserなどでアカウントを作成する際の質問項目も追加する必要がある。
今回の場合、外部キーのclient_idという外部キーを追加の入力項目にしたため、合わせてclientのモデルも設定する必要がある。
加えて、ログインで参照するモデルが従来のauth_userではなくaccounts_userになるため、その旨をsettings.pyに書き加える。

/project/accounts/models.py

class Client(models.Model):
    id = models.BigAutoField(auto_created=True, primary_key=True,
                             serialize=False)
    name = models.CharField(max_length=50)
    updated_at = models.DateTimeField()
    created_at = models.DateTimeField()

...
    def __str__(self):
        return self.name


class CustomUserManager(UserManager):
    def _create_user(self, client_id, username, password, **extra_fields):
        user = self.model(client_id=client_id, username=username, password=password, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, client_id, username, password=None, **extra_fields):
        return self._create_user(client_id, username, password, **extra_fields)

    def create_superuser(self, username, password, **extra_fields):
        return self._create_user(client_id, username, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    id = models.BigAutoField(auto_created=True, primary_key=True,
                             serialize=False)
    username = models.CharField(max_length=20, unique=True)
    client = models.ForeignKey('Client', to_field='id',
                               on_delete=models.CASCADE)
    updated_at = models.DateTimeField(default=timezone.now)
    created_at = models.DateTimeField(default=timezone.now)

    objects = CustomUserManager()

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['client_id']

    def __str__(self):
        return self.name

    def email_user(self, subject, message, from_email=None, **kwargs):
        """Send an email to this user."""
        send_mail(subject, message, from_email, [self.email], **kwargs)
...

/project/project/settings.py

AUTH_USER_MODEL = 'common.User'

3. authenticateを作成

通常通りのログインだと、django.contrib.auth.authenticateで手続きを行い、usernameとpasswordでログインする。 そのため、このauthenticateをオーバーライドさせることが命題になる。 ここではbackend.pyというファイルを作成し、そこにAuthBackend.authenticateを作成した。

/project/accounts/backend.py

from .models import User
from django.contrib.auth.backends import ModelBackend


class AuthBackend(ModelBackend):
    supports_object_permissions = True
    supports_anonymous_user = False
    supports_inactive_user = False


    def authenticate(client_id, username, password):
        if client_id.isdecimal() == False:
            return None
        try:
            user = User.objects.filter(client_id=client_id, username=username).get();
        except User.DoesNotExist:
            return None

        return user if user.check_password(password) else None

4. ログイン用のviewを作成

あとは、ログイン画面の設定をするだけ。
通常通りにauthenticateを使うとdjango.contrib.auth.authenticateを参照されてしまうので、これをAuthBackend.authenticateでオーバーライドさせればOK。

/project/accounts/views.py

from django.contrib.auth import login
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import LoginView, LogoutView
from django.contrib import messages
from django.shortcuts import render, redirect
from .backend import AuthBackend
from .forms import LoginForm


class Login(LoginView):
    form_class = LoginForm
    template_name = 'accounts/login.html'

    def post(self, request):
        client_id = request.POST.get('client_id')
        username = request.POST.get('username')
        password = request.POST.get('password')
        user = AuthBackend.authenticate(client_id=client_id, username=username, password=password)
        if user is not None:
            if user.is_active:
                login(request, user, backend='django.contrib.auth.backends.ModelBackend')
                return redirect('accounts:index')
            else:
                messages.error(request, '入力内容に誤りがあります。')
                return render(request, self.template_name, {'form': self.form_class})
        else:
            messages.error(request, '入力内容に誤りがあります。')
            return render(request, self.template_name, {'form': self.form_class})


class Logout(LoginRequiredMixin, LogoutView):
    template_name = 'accounts/logout.html'

5. その他

あとはtemplatesでaccounts/login.htmlとaccounts/logout.htmlを設定する必要があるけど、そこは割愛。
僕はforms.pyを使って出力したけど、直接HTMLを入力してもいけると思うので、必要な場合は適宜対応してください。