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 支持的哈希算法类。 此列表中的第一个条目(即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

Django中的新功能1.10。

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

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

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

  1. 安装argon2-cffi库 This can be done by running pip install django[argon2], which is equivalent to pip install argon2-cffi (along with any version requirement from Django’s 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 upgrade passwords,请保留和/或添加此列表中的任何条目。

使用bcrypt与Django

Bcrypt是一种流行的密码储存算法,它特意被设计用于长期的密码储存。 由于它需要使用三方的库,所以Django并没有默认使用它。但是既然很多人可能想要使用它,那Django会以最小的代价来支持它。

执行以下步骤来使用Bcrypt作为你的默认储存算法:

  1. 安装bcrypt 库 This can be done by running pip install django[bcrypt], which is equivalent to pip install bcrypt (along with any version requirement from Django’s 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 upgrade passwords,请保留和/或添加此列表中的任何条目。

配置完毕 -- 现在Django会使用Bcrypt作为默认的储存算法。

BCryptPasswordHasher的密码截断

bcrypt的设计者会在72个字符处截断所有的密码,这意味着bcrypt(password_with_100_chars) == bcrypt(password_with_100_chars[:72]) 原生的 BCryptPasswordHasher 并不会做任何的特殊处理, 所以它也会受到这一隐藏密码长度限制的约束。 BCryptSHA256PasswordHasher通过首先使用sha256散列密码来修复此问题。 这样就可以防止密码截断了,所以你还是应该优先考虑BCryptPasswordHasher 这个截断带来的实际效果很微不足道,因为大多数用户不会使用长度超过72的密码,并且即使在72个字符处截断,破解brypt所需的计算能力依然是天文数字。 虽然如此,我们还是推荐使用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. 将你的新的hasher作为第一个元素添加到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 ¶ T0>

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解释

在这个命令行实用程序和其他一些库中,使用与Django使用的值不同的memory_cost参数。 转换由memory_cost == 2 ** memory_cost_commandline T0>。

密码升级

用户登录之后,如果他们的密码没有以首选的密码算法来储存,Django会自动将算法升级为首选的那个。 这意味着Django中旧的安装会在用户登录时自动变得更加安全,并且你可以随意在新的(或者更好的)储存算法发明之后切换到它们。

然而,Django只会升级在 PASSWORD_HASHERS中出现的算法,所以升级到新系统时,你应该确保不要 移除列表中的元素。 如果你移除了,使用列表中没有的算法的用户不会被升级。 当增加(或减少)PBKDF2迭代次数或加密轮数时,哈希密码将被更新。

请注意,如果数据库中的所有密码都未以默认的哈希算法编码,则可能会受到用户枚举计时攻击的影响,这是因为用户编码的密码的用户的登录请求的持续时间有所不同非默认算法和不存在的用户(运行默认的哈希)的登录请求的持续时间。 您可以通过upgrading older password hashes来缓解这一点。

密码升级而不需要登录

如果您有一个现有的数据库具有较旧的,较弱的哈希(如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):
    用户 = apps.get_model('auth', 'User')
    users = 用户.objects.filter(password__startswith='sha1$')
    hasher = PBKDF2WrappedSHA1PasswordHasher()
    for user in users:
        algorithm, salt, sha1_hash = user.密码.split('$', 2)
        user.密码 = 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中包含的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.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
  • 隐窝

编写自己的哈希器

如果您编写自己的密码哈希算法,其中包含诸如迭代次数等工作因素,则应执行harden_runtime(self, 密码, 编码)方法来弥合在encoded密码中提供的工作因子与运算符的默认工作因子之间的运行时间差。 这样可以防止由于用较老的迭代编码的密码的用户的登录请求与不存在的用户(运行默认的哈希表的默认迭代次数)之间的差异导致用户枚举定时攻击。

以PBKDF2为例,如果encoded包含20,000次迭代,并且哈希值的默认iterations为30,000,该方法应通过另外10,000次迭代的PBKDF2运行password

如果您的哈希算符没有工作因素,请将该方法实现为无操作(pass)。

手动管理用户密码

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

check_passwordpasswordencoded[source]

如果你打算通过比较纯文本密码和数据库中哈希后的密码来手动验证用户,要使用check_password()这一便捷的函数。 它接收两个参数:要检查的纯文本密码,和数据库中用户的True字段的完整值。如果二者匹配,返回password ,否则返回False

make_password(password, salt=None, hasher='default')[source]

以当前应用所使用的格式创建哈希密码。 它接受一个必需参数:纯文本密码。 如果你不想使用默认值(PASSWORD_HASHERS设置的首选项),你可以提供salt值和要使用的哈希算法,它们是可选的。 对于每个哈希算法的名称,请参见Included hashers 如果password参数是None,会返回一个不可用的密码(它永远不会被check_password()接受)。

is_password_usable(encoded_password)[source]

检查提供的字符串是否是可以用check_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,它检查密码是否出现在常见密码列表中。 默认情况下,它与包含的1000个常用密码列表进行比较。
  • NumericPasswordValidator,它检查密码是否不是完全数字的。

对于UserAttributeSimilarityValidatorCommonPasswordValidator,我们只是在本例中使用默认设置。 NumericPasswordValidator没有设置。

帮助文本和密码验证器的任何错误总是按照它们在AUTH_PASSWORD_VALIDATORS中列出的顺序返回。

包含的验证器

Django包括四个验证器:

class MinimumLengthValidator(min_length=8)[source]

验证密码是否满足最小长度。 可以使用min_length参数来定制最小长度。

class UserAttributeSimilarityValidator(user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7)[source]

验证密码是否与用户的某些属性完全不同。

user_attributes参数应该是要比较的用户属性的名称的迭代。 如果未提供此参数,则使用默认值:'username', 'first_name', 'last_name', '电子邮件' T4> T0>。 不存在的属性将被忽略。

可以使用max_similarity参数以0到1的等级设置拒绝密码的最小相似度。 设置为0会拒绝所有密码,而设置为1仅拒绝与属性值相同的密码。

class CommonPasswordValidator(password_list_path=DEFAULT_PASSWORD_LIST_PATH)[source]

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

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

class NumericPasswordValidator[source]

验证密码是否不是完全数字。

集成验证

django.contrib.auth.password_validation中有一些函数,你可以从自己的表单或其它代码调用来集成密码验证。 如果你用自定义表单进行密码设置,或者如果你有允许设置密码的API调用,这将非常有用。

validate_passwordpassworduser = Nonepassword_validators = None[source] ¶ T6>

验证密码。 如果所有验证器都找到密码,则返回None 如果一个或多个验证器拒绝该密码,则会从验证器引发一个ValidationError错误消息。

user对象是可选的:如果没有提供,一些验证器可能无法执行任何验证并接受任何密码。

password_changed(password, user=None, password_validators=None)[source]

通知所有验证者密码已被更改。 这可以由诸如防止密码重用的验证器使用。 一旦密码成功更改,就应该调用这个。

对于AbstractBaseUser的子类,当调用set_password()时,密码字段将被标记为“脏”,触发器调用password_changed()之后用户被保存。

password_validators_help_texts(password_validators=None)[source]

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

password_validators_help_text_html T0>( password_validators =无 T1>)¶ T2>

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

get_password_validators(validator_config)[source]

根据validator_config参数返回一组验证器对象。 默认情况下,所有函数都使用AUTH_PASSWORD_VALIDATORS中定义的验证器,但通过使用另一组验证器调用此函数,然后将结果传递给其他函数的password_validators参数,将使用您的自定义验证器集。 当您有一套典型的验证器用于大多数情况时,这种方法非常有用,但也有特殊情况需要自定义集。 如果您始终使用同一组验证器,则不需要使用此功能,因为默认情况下将使用AUTH_PASSWORD_VALIDATORS的配置。

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

请注意,将密码传递给其中一个功能时,应始终为明文密码,而不是散列密码。

编写自己的验证器

如果Django的内置验证器不够,您可以编写自己的密码验证器。 验证器是相当简单的类。 他们必须实行两种方法:

  • 验证(self, 密码, user = None):验证密码。 如果密码有效,则返回None,如果密码无效,则返回ValidationError,并显示错误消息。 您必须能够处理userNone - 如果这意味着您的验证器无法运行,只需返回None,无错误。
  • get_help_text():提供帮助文本,向用户解释要求。

验证器的AUTH_PASSWORD_VALIDATORS中的OPTIONS中的任何项将被传递给构造函数。 所有构造函数参数应具有默认值。

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

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),这将在成功更改密码后调用。 例如,这可以用来防止密码重用。 但是,如果您决定存储用户以前的密码,那么请您永远不要这样做。