Django 中的密码管理

密码管理通常不应被不必要地重新设计,Django努力提供一套安全和灵活的工具来管理用户密码。本文档介绍Django如何存储密码、如何配置存储哈希以及一些使用哈希密码的实用程序。

参见

即使用户可能使用强密码,攻击者也可能能够窃听他们的连接。使用 HTTPS 避免通过纯HTTP连接发送密码(或任何其他敏感数据),因为它们容易受到密码嗅探的攻击。

Django如何存储密码

Django提供了一个灵活的密码存储系统,默认情况下使用pbkdf2。

这个 password 对象的属性 User 对象是以下格式的字符串:

<algorithm>$<iterations>$<salt>$<hash>

这些是用于存储用户密码的组件,由美元符号字符分隔,由以下部分组成:哈希算法、算法迭代次数(工作因子)、随机salt和生成的密码哈希。该算法是Django可以使用的多种单向散列或密码存储算法之一;请参见下文。迭代描述算法在哈希上运行的次数。salt是使用的随机种子,哈希是单向函数的结果。

默认情况下,Django使用 PBKDF2 sha256哈希算法,密码扩展机制由 NIST. 对于大多数用户来说,这应该足够了:它非常安全,需要大量的计算时间才能中断。

但是,根据您的需求,您可以选择不同的算法,甚至可以使用自定义算法来匹配特定的安全情况。同样,大多数用户不需要这样做——如果您不确定,可能不需要这样做。如果确实需要,请继续阅读:

Django通过参考 PASSWORD_HASHERS 布景。这是此Django安装支持的散列算法类的列表。

为了存储密码,Django将使用 PASSWORD_HASHERS 。要使用不同的算法存储新密码,请先将您喜欢的算法放入 PASSWORD_HASHERS

为了验证密码,Django将在列表中找到与存储的密码中的算法名称匹配的散列器。如果存储的密码命名了在 PASSWORD_HASHERS ,试图验证它是否会引发 ValueError

默认值为 PASSWORD_HASHERS 是::

PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
    "django.contrib.auth.hashers.Argon2PasswordHasher",
    "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
    "django.contrib.auth.hashers.ScryptPasswordHasher",
]

这意味着Django将使用 PBKDF2 存储所有密码,但将支持检查用pbkdf2sha1存储的密码, argon2, 和 bcrypt.

接下来的几节将介绍高级用户修改此设置的几种常见方法。

将argon2与django一起使用

Argon2 是2015年的获胜者 Password Hashing Competition ,一个社区组织了公开比赛来评选下一代哈希算法。在定制硬件上进行计算并不比在普通CPU上进行计算更容易。Argon2密码哈希器的默认变体是Argon2id。

Argon2 不是Django的默认值,因为它需要第三方库。然而,密码散列竞争小组建议立即使用argon2,而不是Django支持的其他算法。

要使用Argon2id作为默认存储算法,请执行以下操作:

  1. 安装 argon2-cffi 包裹。这可以通过运行以下命令来完成 python -m pip install django[argon2] ,这相当于 python -m pip install argon2-cffi (以及Django的任何版本要求 setup.cfg )。

  2. 修改 PASSWORD_HASHERS 列出 Argon2PasswordHasher 第一。也就是说,在设置文件中,您将:

    PASSWORD_HASHERS = [
        "django.contrib.auth.hashers.Argon2PasswordHasher",
        "django.contrib.auth.hashers.PBKDF2PasswordHasher",
        "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
        "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
        "django.contrib.auth.hashers.ScryptPasswordHasher",
    ]
    

    保留和/或添加此列表中的任何条目(如果需要Django) upgrade passwords .

使用 bcrypt 与Django

Bcrypt 是一种流行的密码存储算法,专门为长期密码存储而设计。这不是Django使用的默认值,因为它需要使用第三方库,但由于许多人可能希望使用它,Django只需最少的努力就可以支持Bcrypt。

要使用bcrypt作为默认存储算法,请执行以下操作:

  1. 安装 bcrypt 包裹。这可以通过运行以下命令来完成 python -m pip install django[bcrypt] ,这相当于 python -m pip install bcrypt (以及Django的任何版本要求 setup.cfg )。

  2. 修改 PASSWORD_HASHERS 列出 BCryptSHA256PasswordHasher 第一。也就是说,在设置文件中,您将:

    PASSWORD_HASHERS = [
        "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
        "django.contrib.auth.hashers.PBKDF2PasswordHasher",
        "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
        "django.contrib.auth.hashers.Argon2PasswordHasher",
        "django.contrib.auth.hashers.ScryptPasswordHasher",
    ]
    

    保留和/或添加此列表中的任何条目(如果需要Django) upgrade passwords .

就是这样——现在您的django安装将使用bcrypt作为默认存储算法。

vbl.使用 scrypt 和Django

scrypt 在利用一组迭代次数来减缓暴力攻击方面类似于PBKDF2和bcrypt。但是,由于PBKDF2和bcrypt不需要大量内存,因此拥有足够资源的攻击者可以发起大规模并行攻击,以加快攻击过程。 scrypt 与其他基于密码的密钥派生函数相比,专门设计为使用更多内存,以限制攻击者可以使用的并行度,请参见 RFC 7914 了解更多详细信息。

使用 scrypt 作为默认存储算法,请执行以下操作:

  1. 修改 PASSWORD_HASHERS 列出以下内容 ScryptPasswordHasher 第一。也就是说,在设置文件中::

    PASSWORD_HASHERS = [
        "django.contrib.auth.hashers.ScryptPasswordHasher",
        "django.contrib.auth.hashers.PBKDF2PasswordHasher",
        "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
        "django.contrib.auth.hashers.Argon2PasswordHasher",
        "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
    ]
    

    保留和/或添加此列表中的任何条目(如果需要Django) upgrade passwords .

备注

scrypt 需要OpenSSL1.1+。

增加盐熵

大多数口令散列在其口令散列中都包含一个SALT,以防止彩虹表攻击。盐本身是一个随机值,它增加了彩虹表的大小并因此增加了成本,并且当前使用 salt_entropy 中的值。 BasePasswordHasher 。随着计算和存储成本的降低,这个值应该会提高。在实现您自己的密码散列器时,您可以自由地覆盖此值,以便为您的密码散列使用所需的熵级别。 salt_entropy 是以位为单位来衡量的。

实施详情

由于存储盐值的方法不同, salt_entropy 值实际上是一个最小值。例如,值128将提供实际包含131位熵的盐。

提高工作系数

PBKdf2和BCRYPT

PBKDF2和bcrypt算法使用多次迭代或哈希。这故意减慢了攻击者的速度,使对哈希密码的攻击变得更加困难。然而,随着计算能力的增加,迭代的次数也需要增加。我们已经选择了一个合理的缺省值(并将随着Django的每次发布而增加它),但您可能希望根据您的安全需求和可用的处理能力来调整它。为此,您将继承适当算法的子类并重写 iterations 参数(使用 rounds 参数进行子类化)。例如,要增加默认PBKDF2算法使用的迭代次数:

  1. 创建一个子类 django.contrib.auth.hashers.PBKDF2PasswordHasher **

    from django.contrib.auth.hashers import PBKDF2PasswordHasher
    
    
    class MyPBKDF2PasswordHasher(PBKDF2PasswordHasher):
        """
        A subclass of PBKDF2PasswordHasher that uses 100 times more iterations.
        """
    
        iterations = PBKDF2PasswordHasher.iterations * 100
    

    将此保存在项目中的某个位置。例如,您可以将它放入类似 myproject/hashers.py .

  2. 将新哈希添加为 PASSWORD_HASHERS ::

    PASSWORD_HASHERS = [
        "myproject.hashers.MyPBKDF2PasswordHasher",
        "django.contrib.auth.hashers.PBKDF2PasswordHasher",
        "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
        "django.contrib.auth.hashers.Argon2PasswordHasher",
        "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
        "django.contrib.auth.hashers.ScryptPasswordHasher",
    ]
    

就是这样——现在,当您的Django安装使用pbkdf2存储密码时,它将使用更多的迭代。

备注

加密 rounds 是对数功因数,例如12轮平均数 2 ** 12 迭代次数。

阿贡2号

Argon2具有以下可自定义的属性:

  1. time_cost 控制哈希中的迭代次数。

  2. memory_cost 控制哈希计算期间必须使用的内存大小。

  3. parallelism 控制可以并行计算哈希的CPU数量。

这些属性的默认值可能对您来说很好。如果确定密码哈希太快或太慢,可以按以下方式对其进行调整:

  1. 选择 parallelism 为了获得线程数,您可以节省计算哈希的时间。

  2. 选择 memory_cost 为了成为你能留下的记忆。

  3. 调整 time_cost 并测量密码散列所需的时间。挑选一个 time_cost 这对你来说需要一段可以接受的时间。如果 time_cost 设置为1的速度太慢,太低 memory_cost .

memory_cost 解释

argon2命令行实用程序和其他一些库解释 memory_cost 参数与Django使用的值不同。换算公式如下: memory_cost == 2 ** memory_cost_commandline .

scrypt

scrypt 具有以下可自定义的属性:

  1. work_factor 控制散列中的迭代次数。

  2. block_size

  3. parallelism 控制并行运行的线程数。

  4. maxmem 限制哈希计算期间可以使用的最大内存大小。默认为 0 ,这意味着来自OpenSSL库的默认限制。

我们已经选择了合理的默认设置,但您可能希望根据您的安全需求和可用的处理能力对其进行调整。

估计内存使用量

的最低内存要求 scrypt 是::

work_factor * 2 * block_size * 64

所以你可能需要调整一下 maxmem 当更改 work_factorblock_size 价值观。

密码升级

当用户登录时,如果他们的密码存储在首选算法之外,Django将自动将该算法升级到首选算法。这意味着当用户登录时,旧的django安装会自动变得更安全,而且这也意味着当新的(和更好的)存储算法被发明时,您可以切换到新的(和更好的)存储算法。

但是,django只能升级使用中提到的算法的密码。 PASSWORD_HASHERS ,因此,在升级到新系统时,应确保永远不要 去除 此列表中的条目。如果你这样做了,使用未提及算法的用户将无法升级。当增加(或减少)PBKDF2迭代次数、bcrypt轮数或argon2属性数时,哈希密码将被更新。

请注意,如果数据库中的所有密码都不是用默认哈希算法编码的,那么您可能容易受到用户枚举计时攻击,这是因为使用非默认算法编码的密码的用户的登录请求持续时间与不存在的用户的登录请求持续时间(即取消默认哈希)。你可以通过 upgrading older password hashes .

无需登录即可升级密码

如果您的现有数据库具有较旧、较弱的散列,如MD5,您可能希望自己升级这些散列,而不是等待用户登录时进行升级(如果用户不返回您的站点,则可能永远不会进行升级)。在这种情况下,您可以使用“包装的”密码破解程序。

对于本例,我们将迁移一组MD5散列以使用PBKDF2(MD5(Password)),并添加相应的密码散列器,以检查用户在登录时是否输入了正确的密码。我们假设我们使用的是内置 User 模型,并且我们的项目有一个 accounts 应用程序。您可以修改该模式以使用任何算法或自定义用户模型。

首先,我们将添加自定义哈希:

accounts/hashers.py
from django.contrib.auth.hashers import (
    PBKDF2PasswordHasher,
    MD5PasswordHasher,
)


class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
    algorithm = "pbkdf2_wrapped_md5"

    def encode_md5_hash(self, md5_hash, salt, iterations=None):
        return super().encode(md5_hash, salt, iterations)

    def encode(self, password, salt, iterations=None):
        _, _, md5_hash = MD5PasswordHasher().encode(password, salt).split("$", 2)
        return self.encode_md5_hash(md5_hash, salt, iterations)

数据迁移可能看起来像:

accounts/migrations/0002_migrate_md5_passwords.py
from django.db import migrations

from ..hashers import PBKDF2WrappedMD5PasswordHasher


def forwards_func(apps, schema_editor):
    User = apps.get_model("auth", "User")
    users = User.objects.filter(password__startswith="md5$")
    hasher = PBKDF2WrappedMD5PasswordHasher()
    for user in users:
        algorithm, salt, md5_hash = user.password.split("$", 2)
        user.password = hasher.encode_md5_hash(md5_hash, salt)
        user.save(update_fields=["password"])


class Migration(migrations.Migration):
    dependencies = [
        ("accounts", "0001_initial"),
        # replace this with the latest migration in contrib.auth
        ("auth", "####_migration_name"),
    ]

    operations = [
        migrations.RunPython(forwards_func),
    ]

请注意,根据硬件的速度,对于几千个用户来说,此迁移将花费几分钟的时间。

最后,我们将添加 PASSWORD_HASHERS 设置:

mysite/settings.py
PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
    "accounts.hashers.PBKDF2WrappedMD5PasswordHasher",
]

在此列表中包括您的网站使用的任何其他哈希。

包含哈希

Django中包含的哈希的完整列表为:

[
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
    "django.contrib.auth.hashers.Argon2PasswordHasher",
    "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
    "django.contrib.auth.hashers.BCryptPasswordHasher",
    "django.contrib.auth.hashers.ScryptPasswordHasher",
    "django.contrib.auth.hashers.MD5PasswordHasher",
]

相应的算法名称为:

  • pbkdf2_sha256

  • pbkdf2_sha1

  • argon2

  • bcrypt_sha256

  • bcrypt

  • scrypt

  • md5

写你自己的哈希表

如果编写自己的密码散列器,其中包含工作因素(如迭代次数),则应实现 harden_runtime(self, password, encoded) 方法来弥合在 encoded 密码和哈希的默认工作因子。这可以防止用户枚举计时攻击,因为在旧迭代次数中编码密码的用户的登录请求与不存在的用户(运行默认哈希程序的默认迭代次数)之间存在差异。

以pbkdf2为例,如果 encoded 包含20000个迭代和哈希默认值 iterations 是30000,方法应该运行 password 通过另一个10000次的pbkdf2迭代。

如果散列器没有工作因子,则将该方法实现为no op (pass

手动管理用户密码

这个 django.contrib.auth.hashers 模块提供了一组用于创建和验证哈希密码的函数。您可以独立于 User 模型。

check_password(password, encoded, setter=None, preferred='default')[源代码]
acheck_password(password, encoded, asetter=None, preferred='default')[源代码]

Asynchronous versionacheck_password()

如果您想通过比较明文密码和数据库中的散列密码来手动验证用户,请使用便利性函数 check_password() 。它有两个强制参数:要检查的纯文本密码和用户的全值 password 数据库中要检查的字段。它又回来了 True 如果它们匹配, False 否则的话。或者,您可以将可调用的 setter 它接受密码,并在需要重新生成密码时被调用。你也可以通过 preferred 如果您不想使用缺省值(第一个条目为 PASSWORD_HASHERS 设置)。看见 包含哈希 用于每个哈希器的算法名称。

Changed in Django 5.0:

acheck_password() 添加了方法。

make_password(password, salt=None, hasher='default')[源代码]

以此应用程序使用的格式创建哈希密码。它需要一个强制参数:明文形式的密码(字符串或字节)。如果不想使用默认值(第一个条目 PASSWORD_HASHERS 设置)。见 包含哈希 对于每个哈希器的算法名称。如果密码参数为 None ,将返回一个不可用的密码(永远不会被接受的密码) check_password()

is_password_usable(encoded_password)[源代码]

返回 False 如果密码是 User.set_unusable_password() .

密码验证

用户经常选择糟糕的密码。为了帮助缓解这个问题,Django提供了可插拔的密码验证。您可以同时配置多个密码验证器。Django中包含了一些验证器,但您也可以编写自己的验证器。

每个密码验证器都必须提供帮助文本来向用户解释要求,验证给定的密码并在不符合要求时返回错误消息,并可选地定义一个回调,以便在用户的密码更改时得到通知。验证器还可以具有可选设置来微调其行为。

验证由 AUTH_PASSWORD_VALIDATORS 设置。该设置的默认值是空列表,这意味着没有应用任何验证器。在使用默认值创建的新项目中 startproject 模板,默认启用一组验证器。

默认情况下,在表单中使用验证程序重置或更改密码,并在 createsuperuserchangepassword 管理命令。例如,在 User.objects.create_user()create_superuser() ,因为我们假定开发人员(而不是用户)在该级别与Django交互,而且模型验证不会作为创建模型的一部分自动运行。

备注

密码验证可以防止使用多种弱密码。但是,一个密码通过所有验证器的事实并不能保证它是一个强密码。有许多因素会削弱甚至最高级的密码验证器都无法检测到的密码。

启用密码验证

密码验证配置在 AUTH_PASSWORD_VALIDATORS 设置:

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
        "OPTIONS": {
            "min_length": 9,
        },
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]

此示例启用所有四个包含的验证器:

  • UserAttributeSimilarityValidator 它检查密码和用户的一组属性之间的相似性。

  • MinimumLengthValidator ,检查密码是否符合最小长度。这个验证器配置了一个自定义选项:它现在要求最小长度为9个字符,而不是默认的8个字符。

  • CommonPasswordValidator ,检查密码是否出现在常用密码列表中。默认情况下,它与包含20000个常用密码的列表进行比较。

  • NumericPasswordValidator ,它检查密码是否完全是数字。

为了 UserAttributeSimilarityValidatorCommonPasswordValidator ,我们在本例中使用默认设置。 NumericPasswordValidator 没有设置。

帮助文本和来自密码验证器的任何错误始终按照它们在中列出的顺序返回。 AUTH_PASSWORD_VALIDATORS .

包括验证器

Django包括四个验证器:

class MinimumLengthValidator(min_length=8)[源代码]

验证密码是否具有最小长度。最小长度可以使用 min_length 参数。

class UserAttributeSimilarityValidator(user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7)[源代码]

验证密码是否与用户的某些属性完全不同。

这个 user_attributes 参数应该是要比较的用户属性的名称的iteable。如果未提供此参数,则使用默认值: 'username', 'first_name', 'last_name', 'email' . 忽略不存在的属性。

密码的最大允许相似度可以在0.1到1.0的范围内设置, max_similarity 参数。这与以下结果进行了比较 difflib.SequenceMatcher.quick_ratio() 。值0.1表示拒绝密码,除非密码与 user_attributes ,而值1.0仅拒绝与属性值相同的密码。

class CommonPasswordValidator(password_list_path=DEFAULT_PASSWORD_LIST_PATH)[源代码]

验证该密码不是通用密码。这会将密码转换为小写(以进行不区分大小写的比较),并将其与由创建的包含20,000个常见密码的列表进行比较 Royce Williams

这个 password_list_path 可以设置为公用密码的自定义文件的路径。此文件每行应包含一个小写密码,可以是纯文本或gzip。

class NumericPasswordValidator[源代码]

验证密码是否不完全是数字。

集成验证

django.contrib.auth.password_validation 您可以从自己的表单或其他代码调用以集成密码验证。如果您使用自定义表单进行密码设置,或者您有允许设置密码的API调用,那么这可能很有用。

validate_password(password, user=None, password_validators=None)[源代码]

验证密码。如果所有验证器都发现密码有效,则返回 None . 如果一个或多个验证器拒绝密码,则引发 ValidationError 所有来自验证程序的错误消息。

这个 user 对象是可选的:如果没有提供,某些验证器可能无法执行任何验证,并且将接受任何密码。

password_changed(password, user=None, password_validators=None)[源代码]

通知所有验证程序密码已更改。这可以由验证程序使用,例如阻止密码重用的验证程序。一旦成功更改了密码,就应该调用它。

对于子类 AbstractBaseUser ,当调用时,密码字段将标记为“脏”。 set_password() 它会触发一个调用 password_changed() 保存用户后。

password_validators_help_texts(password_validators=None)[源代码]

返回所有验证程序的帮助文本列表。这些向用户解释了密码要求。

password_validators_help_text_html(password_validators=None)

返回一个HTML字符串,其中包含 <ul> . 这在向表单添加密码验证时很有用,因为您可以将输出直接传递给 help_text 窗体字段的参数。

get_password_validators(validator_config)[源代码]

返回一组基于 validator_config 参数。默认情况下,所有函数都使用 AUTH_PASSWORD_VALIDATORS ,但通过使用另一组验证器调用此函数,然后将结果传递到 password_validators 其他函数的参数,将使用自定义的验证器集。当您有一组典型的验证器可用于大多数场景时,这是很有用的,但也有一种特殊情况需要自定义集。如果始终使用同一组验证程序,则不需要使用此函数,因为配置来自 AUTH_PASSWORD_VALIDATORS 默认情况下使用。

的结构 validator_config 与…的结构相同 AUTH_PASSWORD_VALIDATORS . 此函数的返回值可以传递到 password_validators 上面列出的函数的参数。

请注意,如果将密码传递给其中一个函数,则应始终使用明文密码,而不是哈希密码。

编写自己的验证器

如果Django的内置验证器不够,您可以编写自己的密码验证器。验证器有一个相当小的接口。他们必须实施两种方法:

  • validate(self, password, user=None) :验证密码。返回 None 如果密码有效,或引发 ValidationError 如果密码无效,则显示错误消息。你必须能够处理 user 存在 None -如果这意味着验证器无法运行,请返回 None 没有错误。

  • get_help_text() :提供帮助文本以向用户解释要求。

中的任何项目 OPTIONS 在里面 AUTH_PASSWORD_VALIDATORS 对于,验证程序将传递给构造函数。所有构造函数参数都应具有默认值。

下面是验证器的一个基本示例,其中有一个可选设置:

from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _


class MinimumLengthValidator:
    def __init__(self, min_length=8):
        self.min_length = min_length

    def validate(self, password, user=None):
        if len(password) < self.min_length:
            raise ValidationError(
                _("This password must contain at least %(min_length)d characters."),
                code="password_too_short",
                params={"min_length": self.min_length},
            )

    def get_help_text(self):
        return _(
            "Your password must contain at least %(min_length)d characters."
            % {"min_length": self.min_length}
        )

您还可以实现 password_changed(password, user=None ,这将在成功更改密码后调用。例如,它可以用来防止密码重用。但是,如果决定存储用户以前的密码,则不应以明文形式存储。