Бэкенд пользовательской аутентификации Django - "is_valid" вызывает authenticate

Я впервые делаю свой пользовательский бэкенд auth, и в целом он работает, но я не понял пару вещей, и есть некоторое нежелательное, на мой взгляд, поведение.

В принципе, я просто хотел добавить дополнительное обязательное поле - имя домена, которое должно быть заполнено (или выбрано) при входе в систему.

Так, у меня есть пользовательский DomainUser (AbstractUser), который определяет дополнительное поле "domainname"

class DomainUser(AbstractUser):
    domainname = models.CharField(max_length=50)
    REQUIRED_FIELDS = ['domainname', ]
    def __str__(self):
        return self.username
    @property
    def domainusername(self):
        return ''.join([self.domainname, '\\', self.username])

Тут есть мнение:

def domain_login(request):
    if request.method == 'POST':
        login_form = DomainLoginForm(data=request.POST)
        if login_form.is_valid():
            username = login_form.cleaned_data.get('username')
            domainname = login_form.cleaned_data.get('domainname')
            raw_password = login_form.cleaned_data.get('password')
            user = authenticate(username=username, domainname=domainname, password=raw_password)
            if user is not None:
                login(request, user)
                return HttpResponseRedirect(reverse('admin:index'))
            else:
                pass # should return invalid logon page
    else:
        login_form = DomainLoginForm()
    
    context = {
        'login_form': login_form,
    }
    return render(request, 'domain_auth/login.html', context)

Автор:

class DomainLDAPBackend(BaseBackend):

    def authenticate(self, request, username=None, domainname=None, password=None):
        #return DomainUser.objects.get(username__iexact=username, domainname__iexact=domainname)
        return DomainUser.objects.get(username__iexact=username)

    def get_user(self, user_id):
        try:
            return DomainUser.objects.get(pk=user_id)
        except DomainUser.DoesNotExists:
            return None

Auth - это пока что просто заполнитель (он не аутентифицирует использование с доменом, но это временно), это просто для тестирования, прежде чем я подключу сюда свой настоящий модуль auth.

Моя форма входа также имеет поле domainname, и оно работает.

У меня две проблемы: Я заполняю форму входа, и вызывается представление:

  1. It seems that "authenticate" is called 2 times in my "domain_login" view, first time is by "is_valid" method and this first time it passes "domainname" parameter in authenticate as None. Why is it called here? Is it a normal behaviour? I couldn't find anything like this in the documentation, but i suppose there is an error in my code. Second manual call in my view is correct - all parameters are passed as forseen, login works as well.

  2. What's the purpose of get_user method? It's called only after logging on (and while using admin, especially browsing user model), but not while authenticating.

Спасибо.

Попробуйте это:

Бэкэнд аутентификации

# Override ModelBackend instead of BaseBackend, so you get various methods out of the box
class DomainLDAPBackend(ModelBackend):
    # Don' t set default values for username, domainname and password (so they are manadatory)
    def authenticate(self, request, username, domainname, password):
        try:
            # iexact is used by default
            user = DomainUser.objects.get(username=username, domainname=domainname)
            # Checks the password
            if user.check_password(password):
                return user
        except ObjectDoesNotExist:
            return

Если вы хотите, чтобы пользователи могли выбирать имя домена при входе, измените authenticate (я предлагаю запрашивать имя домена при регистрации):

def authenticate(self, request, username, domainname, password):
    try:
        user = DomainUser.objects.get(username=username)
        if user.check_password(password):
            if user.domainname is None:
                user.domainname = domainname
                user.save()
                return user
    except ObjectDoesNotExist:
        return

Виды

def domain_login(request):
    if request.method == 'POST':
        login_form = DomainLoginForm(data=request.POST)
        # There is not need to call login_form.is_valid: access POST data directly, the validation is performed by DomainLDAPBackend
        username = request.POST.get('username')
        domainname = request.POST.get('domainname')
        raw_password = request.POST.get('password')
        user = authenticate(username=username, domainname=domainname, password=raw_password)
        if user is not None:
            login(request, user)
            return HttpResponseRedirect(reverse('admin:index'))
        else:
            pass # Should return invalid login page
    else:
        login_form = DomainLoginForm()
    
    return render(request, 'domain_auth/login.html', {'login_form': login_form})

Обратите внимание, что нет необходимости переопределять ModelBackend.get_user, который не конфликтует с вашей пользовательской моделью пользователя. Этот метод используется Django для получения информации о зарегистрированном пользователе. Не забудьте установить AUTHENTICATION_BACKENDS и AUTH_USER_MODEL в ваших настройках.

Редактировать

Я проанализировал код AuthenticationForm на GitHub и обнаружил, что он уже аутентифицирует пользователя в validate (именно по этой причине ваш исходный код вызывал authenticate два раза). Сначала вам нужно переопределить validate, чтобы по умолчанию не использовать ваш пользовательский метод бэкенда:

class DomainLoginForm(AuthenticationForm):
    def clean(self):
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')
        domainname = self.cleaned_data.get('domainname')

        if username is not None and password is not None and domainname is not None:
            self.user_cache = authenticate(self.request, username=username, domainname=domainname, password=password)
            if self.user_cache is None:
                raise self.get_invalid_login_error()
            else:
                self.confirm_login_allowed(self.user_cache) # Raises an exception if the user is not active, you can override this method

        return self.cleaned_data

Тогда в своих представлениях вы можете просто написать:

def domain_login(request):
    if request.method == 'POST':
        login_form = DomainLoginForm(data=request.POST)
        if login_form.is_valid():
            # If the form is valid, DomainLoginForm.get_user() never returns None
            user = login_form.get_user()
            login(request, user)
            return HttpResponseRedirect(reverse('admin:index'))
    else:
        login_form = DomainLoginForm()
    
    return render(request, 'domain_auth/login.html', {'login_form': login_form})
Вернуться на верх