Django中的密码管理

密码管理通常不应该不必要地重新发明轮子,Django 致力于提供一套安全且灵活的工具来管理用户密码。本文档描述了 Django 如何存储密码、如何配置存储哈希以及一些用于处理哈希密码的实用程序。

另见

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

Django 如何存储密码

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

password 属性 User 对象是一个字符串,格式如下:

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

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

默认情况下,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、argon2bcrypt 存储的密码。

接下来的几节描述了高级用户可能想要修改此设置的几种常见方法。

在 Django 中使用 Argon2

Argon2 是 2015 年 密码哈希竞赛 的获胜者,这是一场由社区组织的公开竞赛,旨在选择下一代哈希算法。它旨在不让在定制硬件上计算比在普通 CPU 上计算更容易。Argon2 密码哈希器的默认变体是 Argon2id。

Argon2 不是 Django 的默认值,因为它需要一个第三方库。但是,密码哈希竞赛小组建议立即使用 Argon2 而不是 Django 支持的其他算法。

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

  1. 安装 argon2-cffi 包。这可以通过运行 python -m pip install django[argon2] 来完成,这等效于 python -m pip install argon2-cffi(以及 Django 的 pyproject.toml 中的任何版本要求)。

  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 升级密码,请保留和/或在此列表中添加任何条目。

在 Django 中使用 bcrypt

Bcrypt 是一种流行的密码存储算法,专门设计用于长期密码存储。它不是 Django 使用的默认值,因为它需要使用第三方库,但是由于许多人可能想要使用它,因此 Django 可以轻松支持 bcrypt。

要将 Bcrypt 用作默认存储算法,请执行以下操作:

  1. 安装 bcrypt 包。这可以通过运行 python -m pip install django[bcrypt] 来完成,这等效于 python -m pip install bcrypt(以及 Django 的 pyproject.toml 中的任何版本要求)。

  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 升级密码,请保留和/或在此列表中添加任何条目。

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

在 Django 中使用 scrypt

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 升级密码,请保留和/或在此列表中添加任何条目。

注意

scrypt 需要 OpenSSL 1.1+。

增加盐的熵

大多数密码哈希在其密码哈希中包含盐,以防止彩虹表攻击。盐本身是一个随机值,它增加了彩虹表的大小和成本,目前在 BasePasswordHasher 中的 salt_entropy 值设置为 128 位。随着计算和存储成本的降低,此值应提高。在实现您自己的密码哈希器时,您可以随意覆盖此值,以便为您的密码哈希使用所需的熵级别。salt_entropy 以位为单位。

实现细节

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

增加工作因子

PBKDF2 和 bcrypt

PBKDF2 和 bcrypt 算法使用多次迭代或轮次进行哈希运算。这会故意减慢攻击者的速度,使针对哈希密码的攻击更加困难。但是,随着计算能力的提高,迭代次数也需要增加。我们选择了一个合理的默认值(并将在每次 Django 版本发布时增加它),但是您可能希望根据您的安全需求和可用处理能力对其进行调整。为此,您需要对相应的算法进行子类化并覆盖iterations参数(在对 bcrypt 哈希器进行子类化时使用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 存储密码时将使用更多迭代。

注意

bcrypt 的rounds是一个对数工作因子,例如 12 轮意味着2 ** 12次迭代。

Argon2

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

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

  2. memory_cost控制哈希计算过程中必须使用的内存大小。

  3. parallelism控制哈希计算可以在多少个 CPU 上并行化。

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

  1. 选择parallelism为您可以用于计算哈希的线程数。

  2. 选择memory_cost为您可以使用的内存大小(KiB)。

  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

因此,当更改work_factorblock_size值时,您可能需要调整maxmem

密码升级

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

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

请注意,如果数据库中的所有密码都没有使用默认哈希器的算法进行编码,则由于使用非默认算法编码密码的用户登录请求的持续时间与不存在的用户(运行默认哈希器)的登录请求的持续时间之间存在差异,您可能会容易受到用户枚举计时攻击。您可以通过升级旧的密码哈希来减轻这种情况。

无需登录即可进行密码升级

如果您有使用旧的、弱的哈希(例如 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包含 20,000 次迭代,而哈希器的默认iterations为 30,000,则该方法应通过另外 10,000 次 PBKDF2 迭代运行password

如果您的哈希器没有工作因子,请将该方法实现为无操作 (pass)。

手动管理用户的密码

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

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

异步版本acheck_password()

如果您想通过比较明文密码和数据库中散列后的密码来手动验证用户,请使用便捷函数check_password()。它需要两个必选参数:要检查的明文密码,以及数据库中用户password字段的完整值用于对比。如果两者匹配,则返回True,否则返回False。您可以选择性地传递一个可调用的setter,它接受密码,并在您需要重新生成密码时调用。您还可以传递preferred来更改哈希算法,如果您不想使用默认算法(PASSWORD_HASHERS设置的第一个条目)。请参见包含的哈希器以了解每个哈希器的算法名称。

Django 5.0 中的更改

添加了acheck_password()方法。(此处应为 `check_password()`,原文有误)

make_password(password, salt=None, hasher='default')[source]

创建此应用程序使用的格式的散列密码。它需要一个必选参数:明文密码(字符串或字节)。您可以选择性地提供salt和要使用的哈希算法,如果您不想使用默认值(PASSWORD_HASHERS设置的第一个条目)。请参见包含的哈希器以了解每个哈希器的算法名称。如果密码参数为None,则返回不可用的密码(一个永远不会被check_password()接受的密码)。

is_password_usable(encoded_password)[source]

如果密码是User.set_unusable_password()的结果,则返回False

密码验证

用户通常会选择较弱的密码。为了帮助减轻这个问题,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,它检查密码是否满足最小长度。此验证器使用自定义选项进行配置:现在它要求最小长度为九个字符,而不是默认的八个字符。

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

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

对于UserAttributeSimilarityValidatorCommonPasswordValidator,在此示例中我们使用默认设置。NumericPasswordValidator没有设置。

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

包含的验证器

Django 包含四个验证器。

class MinimumLengthValidator(min_length=8)[source]

验证密码是否达到最小长度。最小长度可以使用min_length参数自定义。

class UserAttributeSimilarityValidator(user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7)[source]

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

user_attributes参数应是用户属性名称的可迭代对象,用于进行比较。如果未提供此参数,则使用默认值:'username', 'first_name', 'last_name', 'email'。不存在的属性将被忽略。

密码的最大允许相似度可以使用max_similarity参数在 0.1 到 1.0 的范围内设置。这与difflib.SequenceMatcher.quick_ratio()的结果进行比较。值为 0.1 会拒绝密码,除非它们与user_attributes有很大不同,而值为 1.0 只会拒绝与属性值相同的密码。

class CommonPasswordValidator(password_list_path=DEFAULT_PASSWORD_LIST_PATH)[source]

验证密码不是常用密码。这会将密码转换为小写(进行不区分大小写的比较),并根据Royce Williams创建的 20,000 个常用密码列表进行检查。

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

class NumericPasswordValidator[source]

验证密码不是完全由数字组成。

集成验证

django.contrib.auth.password_validation中有一些函数,您可以从您自己的表单或其他代码中调用这些函数来集成密码验证。如果您使用自定义表单设置密码,或者如果您有允许设置密码的 API 调用,这将非常有用。

validate_password(password, user=None, password_validators=None)[source]

验证密码。如果所有验证器都认为密码有效,则返回None。如果一个或多个验证器拒绝密码,则引发一个ValidationError,其中包含来自验证器的所有错误消息。

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

password_changed(password, user=None, password_validators=None)[source]

通知所有验证器密码已更改。这可用于防止密码重用的验证器等。密码成功更改后,应调用此函数一次。

对于AbstractBaseUser的子类,调用set_password()时,密码字段将被标记为“脏”状态,这会在保存用户后触发对password_changed()的调用。

password_validators_help_texts(password_validators=None)[source]

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

password_validators_help_text_html(password_validators=None)

返回一个HTML字符串,其中包含所有帮助文本,这些文本位于<ul>中。当向表单添加密码验证时,这非常有用,因为您可以将输出直接传递到表单字段的help_text参数。

get_password_validators(validator_config)[source]

根据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。您必须能够处理userNone的情况 - 如果这意味着您的验证器无法运行,则返回None表示没有错误。

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

您验证器的AUTH_PASSWORD_VALIDATORS中的OPTIONS中的任何项目都将传递给构造函数。所有构造函数参数都应该具有默认值。

这是一个验证器的基本示例,带有一个可选设置

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),这将在密码成功更改后调用。例如,这可以用于防止密码重用。但是,如果您决定存储用户的先前密码,则决不应该以明文形式存储。

返回顶部