在 Django 认证期间分别验证用户名和密码

6
在django中使用标准身份验证模块时,用户身份验证失败是含糊不清的。换句话说,似乎没有办法区分以下两种情况:
1.用户名有效,密码无效 2.用户名无效
我想显示适当的消息给用户在这两种情况下,而不是单一的“用户名或密码无效...”。
有没有人有简单的方法来做到这一点。问题的核心似乎直接涉及到最低层 - 在django.contrib.auth.backends.ModelBackend类中。这个类的authenticate()方法,它将用户名和密码作为参数,如果身份验证成功,就返回User对象,如果身份验证失败,则返回None。鉴于这段代码处于最低层(好吧,是高于数据库代码的最低层),绕过它似乎会丢失很多代码。
最好的方法是实现一个新的身份验证后端并将其添加到AUTHENTICATION_BACKENDS设置中吗?可以实现一个后端,它返回一个(User,Bool)元组,其中User对象仅在用户名不存在时为None,Bool仅在密码正确时为True。然而,这将打破后端与django.contrib.auth.authenticate()方法的约定(文档记录了成功身份验证时返回User对象,否则返回None)。
也许,这只是担心过头了?无论用户名或密码是否不正确,用户可能都必须前往“丢失密码”页面,因此这可能只是学术上的问题。但我无法阻止自己感到...
编辑:
关于我选择的答案的评论: 我选择的答案是实现此功能的方法。还有另一个答案,下面讨论了这样做的潜在安全影响,我也考虑过它作为被提名的答案。然而,我提名的答案解释了如何实现此功能。基于安全性的答案讨论了是否应该实现此功能,这实际上是一个不同的问题。

选择正确答案时,应始终考虑答案的安全性影响。有人可能会回答有关密码的问题,建议将密码发送到浏览器并使用JavaScript在本地进行检查。这样可以节省带宽,对吧?节省带宽是好事,对吧?但这并不是一个好答案。 - jmucchiello
在某些情况下,是的,但您提供的示例完全过头了。 在这个问题中,我选择的答案确实解释了如何以我认为是“得到 Django 认可”的方式实现此特性。还有足够的评论跨一些其他答案,建议一个答案中概述的安全性问题可能对某些站点需求来说有点过度。 我认为,在某些人如此强烈反对该答案时,这个基于安全的答案不能被标记为正确答案。 - Steg
5个回答

20

您真的不想区分这两种情况。否则,您会给潜在的黑客一个线索,指示用户名是否有效 - 这对于获取欺诈登录是一种重要帮助。


4
你应该注意让错误的用户名和错误的密码无法区分。例如,即使对于错误的用户名和错误的密码显示相同的消息,分别需要0.1秒和0.5秒,黑客也会利用这一点在攻击密码之前先进行用户名的字典攻击。 - John La Rooy
有趣的是,这很有帮助,因为它描述了一种“最佳实践”,而不是特别技术性的解决方案,这让我想知道Django团队是否考虑到了这一点。然而,看起来很遗憾,你必须削弱网站的可用性,只是为了防止这些混蛋试图欺骗你。现在我必须考虑这对我的网站有多重要。 - Steg
@gnibbler - 哇塞,人们真是费尽心思啊!我从未想过人们会做出这样的事情 - 真是一群怪人!不管怎样,出于兴趣,你想发个链接或者几行文字来说明你可能会采取什么措施来避免这种情况吗?谢谢。 - Steg
让我们来看一下公开展示用户名的网站列表。Github, Stackoverflow等等... 你明白了。安全措施怎么了,为什么要玩晦涩?我认为这种方法没有问题,尽管我很谨慎。如果有任何问题,我建议在用户尝试登录X次失败后进行"密码锁定"。我相信某些人会推荐这种方法,因为大多数主要银行和金融机构都使用这种方法。 - Kurtis

2
这不是后端的功能,仅仅是认证表单的问题。只需重写表单以显示每个字段所需的错误即可。编写一个使用新表单的登录视图,并将其设置为默认的登录URL。(实际上我刚在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

我不认为这是真的。AuthenticationForm使用django.contrib.auth.authenticate方法(它遍历所有可用的后端)- 在以下两种情况下返回“None”:1)用户名不正确或2)用户名正确但密码不正确-这里失去了区别。 - Steg
1
承认错误!感谢您的提示- +1 证明我错了!您显然以前做过这个 - 您对Daniel Roseman上面的安全相关答案有什么评论吗? - Steg
除了基于安全性的答案(我还没有决定),我认为这肯定是解决问题的技术方案。经过更深入的挖掘,这本质上描述了文档中的方法http://docs.djangoproject.com/en/dev/ref/forms/validation/。 - Steg
1
如果您的用户觉得方便,我会这样做。安全向量非常小。如果您有论坛,那么大多数用户名可能已知。如果您想为安全性做出有意义的事情,请记录登录尝试并对其进行速率限制,并且不要让管理员使用弱密码。 - Jason Christa

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文件。如果文件中存在该名称,则我们将创建用户,设置密码,然后进行身份验证。

看一下我对丹尼尔答案的评论。时间攻击比你认为的更常见,如果你让用户选择自己的密码,这尤其糟糕。此外,当用户登录时告诉他们上次登录的时间和失败登录的次数可能是个好主意,以防他们注意到什么异常。 - John La Rooy
这与我的答案有什么关系?请注意,我没有告诉他该怎么做,只是告诉他如何区分这两种情况。在大多数情况下,我们并不是保护英国的皇冠珠宝,而是试图减少需要花费时间和金钱来解决的脑残客户服务电话数量。如果您不知道解决问题的具体细节,包括不知道特定客户的政治/社会背景,我不知道您如何评估解决方案的相对价值。 - Peter Rowell
有趣的答案,因为它证实了我的怀疑,即唯一的方法是不使用身份验证模块提供的现有表单、视图和后端。你上面写的本质上是一个新的身份验证后端,它必须返回除了User对象或None之外的其他东西,正如我在问题中所述,这使它不能插入到现有身份验证代码的其余部分中(即登录视图、AuthenticationForm和authenticate()方法将无法使用)。如果这就是需要的,那好吧——写起来并不需要太多的代码。 - Steg
你说得对,这并不难,并且根据网站的人口统计数据,它可以给你更多的选项。在一个网站上,如果用户名检查失败,我们会检查他们是否实际上输入了他们的电子邮件地址——大约有25%的人这样做,即使表单上没有任何提示说它会起作用。在另一个年龄层显著较高的网站上(平均年龄在60多岁以上),这是一场持久战,因为令人惊讶的是,其中很多人甚至无法记住自己的电子邮件地址。是的,真的。 - Peter Rowell

0
def clean_username(self):
    """
    Verifies that the username is available.
    """
    username = self.cleaned_data["username"]
    try:
        user = User.objects.get(username=username)
    except User.DoesNotExist:
        return username
    else:
        raise forms.ValidationError(u"""\
                This username is already registered, 
                please choose another one.\
                """)

你想要做什么? - sleepsort
为什么这里有一个-1?我认为在Django表单中检查用户名是否有效不是一个坏方法。他的语法有问题吗?+1,除非有人能证明这个错误。 - Kurtis

0

这个答案与Django无关,但这是我用来完成此操作的伪代码:

//Query if user exists who's username=<username> and password=<password>

//If true
    //successful login!

//If false
    //Query if user exists who's username=<username>
        //If true
            //This means the user typed in the wrong password
        //If false
            //This means the user typed in the wrong username

抱歉,我的问题应该说明一下——不要陈述显而易见的事情:)但是说真的,我正在寻找一些特定于Django的答案,即是否有任何我没有遇到的Django部分可以让我完成手头的任务。如果这不清楚,请原谅。 - Steg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接