在Django认证中单独验证用户名和密码

6 投票
5 回答
8661 浏览
提问于 2025-04-15 14:59

在使用Django的标准认证模块时,用户认证失败的情况有点模糊。也就是说,没办法区分以下两种情况:

  • 用户名是有效的,但密码无效
  • 用户名无效

我想在这两种情况下给用户显示不同的提示信息,而不是简单地说“用户名或密码无效...”。

有没有人有简单的方法来实现这个?问题的关键似乎在于django.contrib.auth.backends.ModelBackend这个类。这个类的authenticate()方法接收用户名和密码作为参数,如果认证成功就返回用户对象,如果失败则返回None。由于这段代码处于最低层(也就是在数据库代码之上),绕过它的话感觉就像是浪费了很多代码。

最好的办法是不是实现一个新的认证后端,并把它添加到AUTHENTICATION_BACKENDS设置中?可以实现一个返回(User, Bool)元组的后端,其中用户对象只有在用户名不存在时才为None,而Bool只有在密码正确时才为True。不过,这样做会打破后端与django.contrib.auth.authenticate()方法之间的约定(这个约定是成功认证时返回用户对象,失败时返回None,具体可以参考文档)。

也许,这些担忧其实没必要?无论是用户名还是密码错误,用户可能最终还是得去“找回密码”的页面,所以这可能都是多余的。不过,我就是觉得...

编辑:

关于我选择的答案的评论:我选择的答案是实现这个功能的方式。下面还有另一个答案讨论了这样做可能带来的安全隐患,我也考虑过把它选为答案。不过,我选的答案主要是解释了如何实现这个功能。而基于安全的答案则讨论了是否应该实现这个功能,这其实是一个不同的问题。

5 个回答

0

我们在一个使用外部会员订阅服务的网站上遇到了这个问题。基本上,你需要做的是

from django.contrib.auth.models import User

try:
    user = User.objects.get(username=whatever)
    # if you get here the username exists and you can do a normal authentication
except:
    pass # no such username

在我们的情况下,如果用户名不存在,我们就需要去检查一个由外部网站的Perl脚本更新的HTPASSWD文件。如果这个名字在文件里存在,那么我们就会创建这个用户,设置密码,然后进行身份验证。

20

你其实不想把这两种情况区分开来。否则,你就给了潜在的黑客一个线索,让他们知道某个用户名是否有效——这对他们获取非法登录信息帮助很大。

2

这不是后端的功能,而只是认证表单的问题。你只需要重新写一下表单,让它能显示你想要的每个字段的错误信息。然后写一个登录视图,使用你新写的表单,并把它设为默认的登录网址。(其实我刚刚看到Django的一个最新更新,现在你可以直接把自定义表单传给登录视图,这样做起来更简单了。)这大概只需要花5分钟的时间。你需要的所有东西都在django.contrib.auth里面。

为了更清楚,这里是当前的表单:

class AuthenticationForm(forms.Form):
    """
    Base class for authenticating users. Extend this to get a form that accepts
    username/password logins.
    """
    username = forms.CharField(label=_("Username"), max_length=30)
    password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)

    def __init__(self, request=None, *args, **kwargs):
        """
        If request is passed in, the form will validate that cookies are
        enabled. Note that the request (a HttpRequest object) must have set a
        cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before
        running this validation.
        """
        self.request = request
        self.user_cache = None
        super(AuthenticationForm, self).__init__(*args, **kwargs)

    def clean(self):
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')

        if username and password:
            self.user_cache = authenticate(username=username, password=password)
            if self.user_cache is None:
                raise forms.ValidationError(_("Please enter a correct username and password. Note that both fields are case-sensitive."))
            elif not self.user_cache.is_active:
                raise forms.ValidationError(_("This account is inactive."))

        # TODO: determine whether this should move to its own method.
        if self.request:
            if not self.request.session.test_cookie_worked():
                raise forms.ValidationError(_("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in."))

        return self.cleaned_data

    def get_user_id(self):
        if self.user_cache:
            return self.user_cache.id
        return None

    def get_user(self):
        return self.user_cache

添加:

def clean_username(self):
    username = self.cleaned_data['username']
    try:
        User.objects.get(username=username)
    except User.DoesNotExist:
        raise forms.ValidationError("The username you have entered does not exist.")
    return username

撰写回答