Skip to main content

Django中的密码管理

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

参见

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

Django如何存储密码

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

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

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

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

默认情况下,Django使用带有SHA256散列的 PBKDF2 算法,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.contrib.auth.hashers.BCryptPasswordHasher',
]

这意味着Django将使用 PBKDF2 存储所有密码,但将支持检查使用PBKDF2SHA1,argon2bcrypt 存储的密码。

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

使用Argon2和Django

New in Django 1.10.

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.contrib.auth.hashers.BCryptPasswordHasher',
    ]
    

    如果您需要Django到 升级密码,请保留和/或添加此列表中的任何条目。

使用 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.BCryptPasswordHasher',
        'django.contrib.auth.hashers.PBKDF2PasswordHasher',
        'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
        'django.contrib.auth.hashers.Argon2PasswordHasher',
    ]
    

    如果您需要Django到 升级密码,请保留和/或添加此列表中的任何条目。

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

使用BCryptPasswordHasher进行密码截断

bcrypt的设计者截断所有密码为72个字符,这意味着 bcrypt(password_with_100_chars) == bcrypt(password_with_100_chars[:72])。原始 BCryptPasswordHasher 没有任何特殊处理,因此也受到这个隐藏的密码长度限制。 BCryptSHA256PasswordHasher 通过首先使用sha256散列密码来解决这个问题。这防止了密码截断,因此应优先于 BCryptPasswordHasher。这种截断的实际影响是相当微不足道的,因为一般用户没有长度超过72个字符的密码,甚至在72被截断,在任何有用的时间内暴力强制bcrypt所需的计算能力仍然是天文数字。尽管如此,我们建议您使用 BCryptSHA256PasswordHasher 反正“更安全比对不起”的原则。

其他bcrypt实现

还有几个其他实现允许bcrypt与Django一起使用。 Django的bcrypt支持不直接与这些兼容。要升级,您需要将数据库中的哈希值修改为 bcrypt$(raw bcrypt output) 格式。例如:bcrypt$$2a$12$NT0I31Sa7ihGEWpka9ASYrEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy

增加工作因子

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.contrib.auth.hashers.BCryptPasswordHasher',
    ]
    

就是这样 - 现在你的Django安装将使用PBKDF2存储密码时使用更多的迭代。

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

密码升级

当用户登录时,如果他们的密码与除首选算法以外的任何其他存储在一起,Django会自动将算法升级到首选算法。这意味着,旧的安装的Django将获得自动更安全的用户登录,这也意味着你可以切换到新的(和更好的)存储算法发明时。

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

请注意,如果数据库中的所有密码都未使用默认哈希算法进行编码,则您可能会遇到用户枚举计时攻击,因为用户的登录请求的持续时间与密码在非默认算法和对不存在的用户(运行默认哈希)的登录请求的持续时间。你可以通过 升级旧密码散列 缓解这一点。

Changed in Django 1.9:

添加更改bcrypt轮次数时的密码更新。

密码升级,无需登录

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

对于此示例,我们将迁移SHA1散列的集合以使用PBKDF2(SHA1(密码)),并添加相应的密码哈希以检查用户是否在登录时输入正确的密码。我们假设我们使用内置的 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(PBKDF2WrappedSHA1PasswordHasher, self).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

写你自己的哈希

New in Django 1.9.3.

如果您编写自己的密码哈希,其中包含工作因素(例如多次迭代),则应实施 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,否则返回 False

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

以此应用程序使用的格式创建散列密码。它需要一个强制参数:明文密码。或者,如果您不想使用默认值(PASSWORD_HASHERS 设置的第一个条目),您可以提供盐和散列算法来使用。有关每个哈希算法的算法名称,请参见 包括哈希。如果密码参数是 None,则返回不可用的密码(check_password() 不会接受的密码)。

is_password_usable(encoded_password)

检查给定的字符串是否是具有针对 check_password() 验证的机会的哈希密码。

密码验证

New in Django 1.9.

用户经常选择不良的密码。为了帮助缓解这个问题,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,它检查密码是否出现在常用密码列表中。默认情况下,它会与包含的1000个常用密码的列表进行比较。

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

class CommonPasswordValidator(password_list_path=DEFAULT_PASSWORD_LIST_PATH)

验证密码是否不是通用密码。默认情况下,这将检查由 Mark Burnett 创建的1000个常用密码的列表。

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 中定义的验证器,但是通过使用另一组验证器调用此函数,然后将结果传递给其他函数的 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_VALIDATORSOPTIONS 中您的验证器的任何项目将被传递给构造函数。所有构造函数参数应该有一个默认值。

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

from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _

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