首页 > 解决方案 > Django 自定义身份验证后端 - “is_valid”调用身份验证

问题描述

我是第一次做我的自定义身份验证后端,它通常可以工作,但我无法理解一两件事,并且在我看来有一些不需要的行为。

基本上我只是想添加一个额外的必填字段 - 必须在登录时填写(或选择)的域名。

所以我有我的自定义域用户(AbstractUser),它定义了额外的“域名”字段

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 模块之前进行测试。

我的登录表单也有域名字段,并且可以正常工作。

我有两个问题:我填写了登录表单并调用了视图:

  1. 在我的“domain_login”视图中,似乎“authenticate”被调用了 2 次,第一次是通过“is_valid”方法,而这第一次它在身份验证中将“domainname”参数传递为 None。为什么叫这里?这是正常行为吗?我在文档中找不到类似的东西,但我想我的代码中有错误。在我看来,第二次手动调用是正确的——所有参数都按预期传递,登录也可以。

  2. get_user 方法的目的是什么?它仅在登录后调用(并且在使用管理员时,尤其是浏览用户模型时),而不是在身份验证时调用。

谢谢你。

标签: python-3.xdjango

解决方案


试试这个:

身份验证后端

# 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_BACKENDSAUTH_USER_MODEL进行设置。

编辑

我分析了GitHub 上AuthenticationForm的代码,发现它已经在对用户进行身份验证(这就是您的原始代码调用两次的原因)。首先,您需要覆盖默认情况下不要使用您的自定义后端方法:validateauthenticatevalidate

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})

推荐阅读