Django中的密码管理

密码管理通常不应该被重新再设计,Django 努力提供了一个安全且灵活的管理用户密码的工具。这篇文档描述了 Django 如何存储密码,如何配置存储哈希,和一些使用哈希密码的工具。

参见

即使用户使用了很强壮的密码,攻击者还是可以窃听他们的网络链接。用户使用 HTTPS 可以避免通过纯 HTTP 链接发送密码(或其他一些敏感数据),因为它们很容易被密码嗅探。

Django 如何存储密码

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

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

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

这些是用来存储用户密码的插件,以美元符号分隔,包括:哈希算法,算法迭代次数(工作因子),随机 Salt 和最终的密码哈希值。该算法是 Django 可以使用的单向哈希或密码存储算法中的一种;见下文。迭代描述了算法在哈希上运行的次数。Salt 是所使用的随机种子,哈希是单向函数的结果。

默认情况下,Django 使用带有 SHA256 哈希的 PBKDF2 算法,它是 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 年 Password Hashing Competition 的获胜者,这是一个由社区组织的开放式竞赛,旨在选择下一代哈希算法。它的设计目标是在自定义硬件上计算不应该比在普通 CPU 上计算容易。Argon2 密码哈希器的默认变种是 Argon2id。

Argon2 并不是 Django 的默认首选,因为它依赖第三方库。尽管哈希密码竞赛主办方建议立即使用 Argon2 ,而不是 Django 提供的其他算法。

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

  1. Install the argon2-cffi package. This can be done by running python -m pip install django[argon2], which is equivalent to python -m pip install argon2-cffi (along with any version requirement from Django's 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 升级密码( upgrade passwords ),请保留或添加这个列表中的任何条目。

在 Django 中使用 bcrypt

Bcrypt 是一个非常流行的密码存储算法,尤其是为长期密码存储设计。Django 默认不使用它,因为它需要使用第三方库,但由于很多人想使用它,Django 只需要很少的努力就能支持 bcrypt。

使用 Bcrypt 作为你的默认存储算法,需要以下步骤:

  1. Install the bcrypt package. This can be done by running python -m pip install django[bcrypt], which is equivalent to python -m pip install bcrypt (along with any version requirement from Django's 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 升级密码( upgrade passwords ),请保留或添加这个列表中的任何条目。

现在 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 升级密码( upgrade passwords ),请保留或添加这个列表中的任何条目。

备注

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 。如果设置为1的 time_cost 慢的无法接受,则调低 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) 方法来消除编码密码时提供的工作因子和默认的哈希工作因子之间的运行时间差。这样可以防止用户枚举时间攻击,因为旧的迭代次数中对密码编码的用户与不存在的用户(运行默认哈希的默认迭代次数)在登录时存在差异。

以 PDKDF2 为例,如果编码包含20000次迭代,并且默认哈希迭代是30000,那么该方法应该通过另外的10000次迭代的 PBKDF2 运行密码。

如果你的哈希没有工作因子,可以将该方法实现为 no-op (pass) 。

手动管理用户的密码

The django.contrib.auth.hashers module provides a set of functions to create and validate hashed passwords. You can use them independently from the User model.

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 来更改哈希算法。请参考 已包含的哈希 以获取每个哈希器的算法名称。

Changed in Django 5.0:

acheck_password() 方法已添加。

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

通过此应用的格式创建一个哈希密码。它需要一个必需的参数:纯文本密码(字符串或字节)。或者,如果你不想使用默认配置( PASSWORD_HASHERS 配置的首个条目 ),那么可以提供 salt 和 使用的哈希算法。有关每个哈希的算法名,可查看 已包含的哈希 。如果密码参数是 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 用来检查密码是否符合最小长度。这个验证器可以自定义设置:它现在需要最短9位字符,而不是默认的8个字符。

  • CommonPasswordValidator 检查密码是否在常用密码列表中。默认情况下,它会与列表中的2000个常用密码作比较。

  • 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 的值只会拒绝与属性值相同的密码。

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

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

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() 是会将密码字段标记为 "dirty" ,这会在用户保存后调用 password_changed()

password_validators_help_texts(password_validators=None)[源代码]

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

password_validators_help_text_html(password_validators=None)

返回一个``<ul>`` ,包含所有帮助文案的 HTML 字符串。这在表单中添加密码验证时有帮助,因为你可以直接将输出传递到表单字段的 help_text 参数。

get_password_validators(validator_config)[源代码]

返回一个基于 validator_config 的验证器对象的集合。默认情况下,所有函数使用 AUTH_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) ,在密码修改成功后调用。比如说用来防止密码重用。但是,如果你决定存储用户之前的密码,则不应该以明文形式存储。

Back to Top