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安装支持的哈希算法类的列表。此列表中的第一个条目(即, settings.PASSWORD_HASHERS[0] )将用于存储密码,并且所有其他条目都是可用于检查现有密码的有效散列值。这意味着如果你想使用不同的算法,你需要修改 PASSWORD_HASHERS 在列表中首先列出您的首选算法。

默认值为 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存储的密码, argon2, 和 bcrypt.

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

将argon2与django一起使用

Argon2 是2015年的冠军 Password Hashing Competition 社区组织公开竞争,选择下一代哈希算法。它的设计并不比在普通CPU上计算更容易在定制硬件上计算。

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

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

  1. 安装 argon2-cffi library . 这可以通过运行 pip install django[argon2] ,相当于 pip install argon2-cffi (以及Django的任何版本要求) setup.py

  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) upgrade passwords .

使用 bcrypt 与Django

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

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

  1. 安装 bcrypt library . 这可以通过运行 pip install django[bcrypt] ,相当于 pip install bcrypt (以及Django的任何版本要求) setup.py

  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) upgrade passwords .

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

提高工作系数

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. 将新哈希添加为 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 为了成为你能留下的记忆。
  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只能升级使用中提到的算法的密码。 PASSWORD_HASHERS ,因此,在升级到新系统时,应确保永远不要 去除 此列表中的条目。如果这样做,使用未提及算法的用户将无法升级。当增加(或减少)pbkdf2迭代次数或bcrypt循环次数时,将更新哈希密码。

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

无需登录即可升级密码

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

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

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

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)

数据迁移可能看起来像:

accounts/migrations/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),
    ]

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

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

mysite/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 包含20000个迭代和哈希默认值 iterations 是30000,方法应该运行 password 通过另一个10000次的pbkdf2迭代。

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

手动管理用户密码

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

check_password(password, encoded)[源代码]

如果要通过将纯文本密码与数据库中的哈希密码进行比较来手动验证用户,请使用便利功能 check_password() . 它需要两个参数:要检查的纯文本密码和用户的 password 要检查的数据库中的字段,并返回 True 如果它们匹配, False 否则。

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的比例设置 max_similarity 参数。设置为0将拒绝所有密码,而设置为1将只拒绝与属性值相同的密码。

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

验证密码是否不是普通密码。这会将密码转换为小写(进行不区分大小写的比较),并根据由 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 ,这将在成功更改密码后调用。例如,它可以用来防止密码重用。但是,如果决定存储用户以前的密码,则不应以明文形式存储。