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

在 Django 中使用bcrypt

Bcrypt 是一种流行的密码存储算法,专门设计用于长期密码存储。由于它需要使用第三方库,所以它不是 Django 使用的默认算法,但由于许多人可能希望使用它,因此 Django 支持 bcrypt,且几乎不需要付出任何努力。

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

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

  2. 修改 PASSWORD_HASHERSBCryptSHA256PasswordHasher 列为第一位。也就是说,在你的设置文件中,你应该这样写

    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_HASHERSScryptPasswordHasher 列为第一位。也就是说,在你的设置文件中

    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,则该方法应通过 PBKDF2 的另外 10,000 次迭代来运行 password

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

手动管理用户的密码

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

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

异步版本acheck_password()

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

Django 5.0 中已更改

已添加 acheck_password() 方法。

make_password(password, salt=None, hasher='default')

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

is_password_usable(encoded_password)

如果密码是 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)

验证密码是否达到最小长度。最小长度可以通过 min_length 参数进行定制。

class UserAttributeSimilarityValidator(user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7)

验证密码与用户的某些属性有足够的差异。

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

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

在 Django 2.2.26 中更改

max_similarity 参数的最小值限制为 0.1。

class CommonPasswordValidator(password_list_path=DEFAULT_PASSWORD_LIST_PATH)

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

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

在 Django 4.2 中更改

20,000 个常见密码列表已更新为最新版本。

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() 时,密码字段将标记为“dirty”,这会在保存用户后触发对 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。你必须能够处理 userNone 的情况 - 如果这意味着你的验证器无法运行,则返回 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),它将在成功更改密码后调用。例如,这可用于防止重复使用密码。但是,如果您决定存储用户的先前密码,则切勿以明文形式存储。

返回顶部