Django中的密码管理

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

也可以看看

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

Django如何存储密码

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

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

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

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

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

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

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中的密码。

接下来的几节将介绍一些高级用户可能想要修改此设置的常用方法。

在Django中使用Argon2

Argon2 is the winner of the 2015 Password Hashing Competition, a community organized open competition to select a next generation hashing algorithm. 它的设计在定制硬件上比在普通CPU上计算更容易。

Argon2 is not the default for Django because it requires a third-party library. 然而,Password Hashing Competition面板建议立即使用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 is a popular password storage algorithm that’s specifically designed for long-term password storage. 这不是Django使用的默认设置,因为它需要使用第三方库,但是由于许多人可能想要使用Django,因此Django支持最简单的bcrypt。

要将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]) t3 > T0>。 原始的BCryptPasswordHasher没有任何特殊的处理,因此也受到这个隐藏的密码长度的限制。 BCryptSHA256PasswordHasher fixes this by first hashing the password using sha256. 这样可以防止密码截断,因此应优先于BCryptPasswordHasher 这种截断的实际分支是相当有限的,因为普通用户没有长度超过72个字符的密码,甚至在72被截断,在任何有用的时间量内进行加密所需的计算能力仍然是天文数字。 尽管如此,我们仍然建议您使用BCryptSHA256PasswordHasher,以“比对不起”更安全的原则。

其他的bcrypt实现

有几个其他的实现允许bcrypt与Django一起使用。 Django的bcrypt支持与这些不直接兼容。 To upgrade, you will need to modify the hashes in your database to be in the form 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',
    ]
    

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

Argon2 ¶ T0>

Argon2有三个可以自定义的属性:

  1. time_cost controls the number of iterations within the hash.
  2. memory_cost controls the size of memory that must be used during the computation of the hash.
  3. parallelism controls how many CPUs the computation of the hash can be parallelized on.

这些属性的默认值可能适合你。 如果您确定密码哈希太快或太慢,可以按如下方式调整它:

  1. 选择parallelism为可以计算散列的线程数。
  2. 选择memory_cost作为您可以备用的内存的KiB。
  3. 调整time_cost并测量密码所花费的时间。 选一个可以接受的时间给你的time_cost 如果time_cost设置为1是不可接受的缓慢,请降低memory_cost

memory_cost interpretation

argon2命令行实用程序和其他一些库以与Django使用的值不同的方式解释memory_cost参数。 The conversion is given by memory_cost == 2 ** memory_cost_commandline.

密码升级

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

However, Django can only upgrade passwords that use algorithms mentioned in PASSWORD_HASHERS, so as you upgrade to new systems you should make sure never to remove entries from this list. 如果这样做,使用未提及算法的用户将无法升级。 散列密码将在增加(或减少)PBKDF2迭代次数或bcrypt轮次时更新。

请注意,如果数据库中的所有密码均未使用默认的哈希算法进行编码,则由于用户的登录请求持续时间与密码中编码的密码之间的差异,您可能容易受到用户枚举计时攻击非默认算法以及不存在用户(运行默认散列器)的登录请求的持续时间。 您可以通过upgrading older password hashes来缓解这个问题。

密码升级,无需登录

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

对于这个例子,我们将迁移一个SHA1散列集合来使用PBKDF2(SHA1(密码)),并添加相应的密码散列器来检查用户在登录时是否输入了正确的密码。 我们假设我们正在使用内置的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),
    ]

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

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

mysite的/ settings.py
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'accounts.hashers.PBKDF2WrappedSHA1PasswordHasher',
]

包括您的网站在此列表中使用的其他哈斯。

包含hashers

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
  • 隐窝

写你自己的哈希

如果你编写自己的密码散列器,其中包含一个工作因素,如迭代次数,你应该实现一个harden_runtime(self, password, encoded )方法来桥接encoded密码中提供的工作因子与散列器的默认工作因子之间的运行时间间隔。 这样可以防止用户枚举计时攻击,这是由于用户的登录请求与旧版迭代中编码的密码与不存在的用户(运行默认散列器的默认迭代次数)之间的差异造成的。

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

如果您的hasher没有工作因素,请将该方法实施为无操作(pass)。

手动管理用户的密码

django.contrib.auth.hashers模块提供了一组函数来创建和验证散列的密码。 您可以独立于User模型使用它们。

check_passwordpasswordencoded[source]

如果要通过将明文密码与数据库中的散列密码进行比较来手动验证用户,请使用便捷功能check_password() It takes two arguments: the plain-text password to check, and the full value of a user’s password field in the database to check against, and returns True if they match, False otherwise.

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, which checks the similarity between the password and a set of attributes of the user.
  • MinimumLengthValidator, which simply checks whether the password meets a minimum length. 这个验证器配置了一个自定义选项:它现在需要的最小长度是九个字符,而不是默认的八个。
  • CommonPasswordValidator, which checks whether the password occurs in a list of common passwords. 默认情况下,它与一个包含1000个常用密码的列表进行比较。
  • NumericPasswordValidator, which checks whether the password isn’t entirely numeric.

对于UserAttributeSimilarityValidatorCommonPasswordValidator,我们只是使用这个例子中的默认设置。 NumericPasswordValidator has no settings.

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

包含验证器

Django包含四个验证器:

MinimumLengthValidator(min_length=8)[source]

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

UserAttributeSimilarityValidatoruser_attributes = DEFAULT_USER_ATTRIBUTESmax_similarity = 0.7[source]

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

The user_attributes parameter should be an iterable of names of user attributes to compare to. 如果未提供此参数,则使用默认值:'username', 'first_name', 'last_name', '电子邮件' T4> T0>。 不存在的属性将被忽略。

被拒绝密码的最小相似度可以用max_similarity参数以0到1的比例进行设置。 设置为0将拒绝所有密码,而设置为1则仅拒绝与属性值相同的密码。

CommonPasswordValidator(password_list_path=DEFAULT_PASSWORD_LIST_PATH)[source]

验证密码是否不是通用密码。 这会将密码转换为小写(做一个不区分大小写的比较),并根据Mark Burnett创建的1000个通用密码列表进行检查。

The password_list_path can be set to the path of a custom file of common passwords. 这个文件应该包含每行一个小写的密码,可以是纯文本或gzip。

NumericPasswordValidator[source]

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

整合验证

django.contrib.auth.password_validation中有几个函数,您可以从您自己的表单或其他代码调用以集成密码验证。 例如,如果您使用自定义表单进行密码设置,或者您的API调用允许设置密码,则此功能非常有用。

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

验证密码。 如果所有验证器都发现密码有效,则返回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的内置验证器不够用,您可以编写自己的密码验证器。 验证器是相当简单的类。 他们必须实施两种方法:

  • validate(self, password, user = None):验证密码。 如果密码有效,则返回None;如果密码无效,则返回错误消息ValidationError 您必须能够处理userNone - 如果这意味着您的验证程序无法运行,则仅返回None,以免发生错误。
  • get_help_text(): provide a help text to explain the requirements to the user.

您的验证器中的AUTH_PASSWORD_VALIDATORSOPTIONS中的任何项都将传递给构造函数。 所有的构造函数参数都应该有一个默认值。

下面是一个验证器的基本示例,带有一个可选设置:

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}
        )

You can also implement password_changed(password, user=None), which will be called after a successful password change. 例如,这可以用来防止密码重用。 但是,如果您决定存储用户以前的密码,则绝对不能以明文形式存储。