Django 中的密码管理 — Django 文档

来自菜鸟教程
Django/docs/3.2.x/topics/auth/passwords
跳转至:导航、​搜索

Django 中的密码管理

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

也可以看看

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


Django 如何存储密码

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

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

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

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

默认情况下,Django 使用 PBKDF2 算法和 SHA256 哈希,这是 NIST 推荐的密码拉伸机制。 这对于大多数用户来说应该足够了:它非常安全,需要大量的计算时间才能破解。

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

Django 通过参考 :setting:`PASSWORD_HASHERS` 设置来选择要使用的算法。 这是此 Django 安装支持的散列算法类列表。 此列表中的第一个条目(即 settings.PASSWORD_HASHERS[0])将用于存储密码,所有其他条目都是可用于检查现有密码的有效哈希值。 这意味着如果你想使用不同的算法,你需要修改 :setting:`PASSWORD_HASHERS` 以在列表中首先列出你喜欢的算法。

:setting:`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 将使用 PBKDF2 来存储所有密码,但将支持检查使用 PBKDF2SHA1、argon2bcrypt 存储的密码。

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

在 Django 中使用 Argon2

Argon2 是 2015 年 密码哈希大赛 的获胜者,这是一个社区组织的公开比赛,以选择下一代哈希算法。 它的设计目的是在自定义硬件上进行计算并不比在普通 CPU 上计算更容易。

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

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

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

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


在 Django 中使用 bcrypt

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

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

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

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

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


增加盐熵

3.2 版中的新功能。


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

实现细节

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


增加工作系数

PBKDF2 和 bcrypt

PBKDF2 和 bcrypt 算法使用多次迭代或散列轮次。 这故意减慢攻击者的速度,使对散列密码的攻击更加困难。 但是,随着计算能力的增加,需要增加迭代次数。 我们选择了一个合理的默认值(并且会随着 Django 的每个版本增加它),但是您可能希望根据您的安全需求和可用的处理能力来调整它。 为此,您将对适当的算法进行子类化并覆盖 iterations 参数。 例如,要增加默认 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. 添加您的新哈希作为 :setting:`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 安装在使用 PBKDF2 存储密码时将使用更多迭代。


氩气2

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 给出。


密码升级

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

然而,Django 只能升级使用 :setting:`PASSWORD_HASHERS` 中提到的算法的密码,所以当你升级到新系统时,你应该确保永远不要从这个列表中 删除 条目。 如果这样做,使用未提及算法的用户将无法升级。 当增加(或减少)PBKDF2 迭代次数、bcrypt 轮次或 argon2 属性时,将更新散列密码。

请注意,如果您的数据库中的所有密码都没有以默认哈希算法进行编码,那么您可能容易受到用户枚举计时攻击的影响,因为用户的登录请求持续时间与密码编码为非默认算法和不存在用户(运行默认散列器)的登录请求的持续时间。 您可以通过 升级旧密码哈希 来缓解这种情况。


无需登录即可升级密码

如果您现有的数据库具有较旧的弱散列(例如 MD5 或 SHA1),您可能希望自己升级这些散列,而不是等待用户登录时发生升级(如果用户不登录,则可能永远不会发生)返回您的网站)。 在这种情况下,您可以使用“包装的”密码哈希器。

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

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

帐户/ hashers.py

from django.contrib.auth.hashers import (
    PBKDF2PasswordHasher, SHA1PasswordHasher,
)


class PBKDF2WrappedSHA1PasswordHasher(PBKDF2PasswordHasher):
    algorithm = 'pbkdf2_wrapped_sha1'

    def encode_sha1_hash(self, sha1_hash, salt, iterations=None):
        return super().encode(sha1_hash, salt, iterations)

    def encode(self, password, salt, iterations=None):
        _, _, sha1_hash = SHA1PasswordHasher().encode(password, salt).split('$', 2)
        return self.encode_sha1_hash(sha1_hash, salt, iterations)

数据迁移可能类似于:

帐户/迁移/0002_migrate_sha1_passwords.py

from django.db import migrations

from ..hashers import PBKDF2WrappedSHA1PasswordHasher


def forwards_func(apps, schema_editor):
    User = apps.get_model('auth', 'User')
    users = User.objects.filter(password__startswith='sha1$')
    hasher = PBKDF2WrappedSHA1PasswordHasher()
    for user in users:
        algorithm, salt, sha1_hash = user.password.split('$', 2)
        user.password = hasher.encode_sha1_hash(sha1_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),
    ]

请注意,对于数千名用户,此迁移将花费几分钟的时间,具体取决于您的硬件速度。

最后,我们将添加一个 :setting:`PASSWORD_HASHERS` 设置:

我的网站/settings.py

PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'accounts.hashers.PBKDF2WrappedSHA1PasswordHasher',
]

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


包含哈希器

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.SHA1PasswordHasher',
    'django.contrib.auth.hashers.MD5PasswordHasher',
    'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
    'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
    'django.contrib.auth.hashers.CryptPasswordHasher',
]

对应的算法名称为:

  • pbkdf2_sha256
  • pbkdf2_sha1
  • argon2
  • bcrypt_sha256
  • bcrypt
  • sha1
  • md5
  • unsalted_sha1
  • unsalted_md5
  • crypt


编写自己的哈希器

如果您编写自己的密码散列器,其中包含诸如多次迭代之类的工作因素,则应实施 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)
如果您想通过将纯文本密码与数据库中的散列密码进行比较来手动验证用户,请使用便利函数 check_password()。 它需要两个参数:要检查的纯文本密码,以及要检查的数据库中用户 password 字段的完整值,如果匹配则返回 True,[ X180X] 否则。
make_password(password, salt=None, hasher='default')

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

3.1 版更改: 如果不是 None,则 password 参数必须是字符串或字节。

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


密码验证

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

每个密码验证器都必须提供帮助文本来向用户解释要求,验证给定的密码并在不满足要求时返回错误消息,并可选择接收已设置的密码。 验证器还可以有可选设置来微调他们的行为。

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

默认情况下,验证器在表单中用于重置或更改密码以及 :djadmin:`createsuperuser`:djadmin:`changepassword` 管理命令。 验证器不应用于模型级别,例如在 User.objects.create_user()create_superuser() 中,因为我们假设开发人员,而不是用户,在该级别与 Django 进行交互,并且模型验证也没有作为创建模型的一部分自动运行。

笔记

密码验证可以防止使用多种类型的弱密码。 然而,密码通过所有验证器的事实并不能保证它是一个强密码。 有许多因素可以削弱密码,即使是最先进的密码验证器也无法检测到这些因素。


启用密码验证

密码验证在 :setting:`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,检查密码是否出现在常用密码列表中。 默认情况下,它与包含的 20,000 个常用密码列表进行比较。
  • NumericPasswordValidator,检查密码是否不完全是数字。

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

密码验证器的帮助文本和任何错误总是按照它们在 :setting:`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 的范围内设置被拒绝密码的最小相似度。 0 设置拒绝所有密码,而 1 设置仅拒绝与属性值相同的密码。

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() 时,密码字段将被标记为“脏”,这会在用户保存后触发对 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 参数返回一组验证器对象。 默认情况下,所有函数都使用 :setting:`AUTH_PASSWORD_VALIDATORS` 中定义的验证器,但是通过使用一组备用验证器调用此函数,然后将结果传递到 password_validators 参数中其他功能,将使用您的自定义验证器集。 当您有一组典型的验证器用于大多数场景时,这很有用,但也有需要自定义集的特殊情况。 如果您始终使用相同的验证器集,则无需使用此功能,因为默认使用 :setting:`AUTH_PASSWORD_VALIDATORS` 中的配置。

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

请注意,在将密码传递给这些函数之一的地方,这应该始终是明文密码 - 而不是散列密码。


编写自己的验证器

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

  • validate(self, password, user=None):验证密码。 如果密码有效,则返回 None,如果密码无效,则引发 ValidationError 并带有错误消息。 您必须能够处理 user 成为 None - 如果这意味着您的验证器无法运行,请返回 None 以确保没有错误。
  • get_help_text():提供帮助文本向用户解释需求。

验证器的 :setting:`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),它会在成功更改密码后调用。 例如,这可用于防止密码重复使用。 但是,如果您决定存储用户以前的密码,则永远不要以明文形式存储。