密码管理通常不应该被重新再设计,Django 努力提供了一个安全且灵活的管理用户密码的工具。这篇文档描述了 Django 如何存储密码,如何配置存储哈希,和一些使用哈希密码的工具。
参见
即使用户使用了很强壮的密码,攻击者还是可以窃听他们的网络链接。用户使用 HTTPS 可以避免通过纯 HTTP 链接发送密码(或其他一些敏感数据),因为它们很容易被密码嗅探。
Django 提供灵活的密码存储系统,默认使用 PBKDF2。
The password
attribute of a
User
object is a string in this format:
<algorithm>$<iterations>$<salt>$<hash>
这些是用来存储用户密码的插件,以美元符号分隔,包括:哈希算法,算法迭代次数(工作因子),随机 Salt 和最终的密码哈希值。该算法是 Django 可以使用的单向哈希或密码存储算法中的一种;见下文。迭代描述了算法在哈希上运行的次数。Salt 是所使用的随机种子,哈希是单向函数的结果。
默认情况下,Django 使用带有 SHA256 哈希的 PBKDF2 算法,它是 NIST 推荐的密码延展机制。它足够安全,需要大量的运算时间才能破解,这对大部分用户来说足够了。
但是,根据你的需求,你可以选择不同的算法,甚至使用自定义的算法来匹配特定的安全场景。再次强调,大部分用户没必要这么做,如果你不确定的话,很可能并不需要。如果你坚持要做,请继续阅读:
Django chooses the algorithm to use by consulting the
PASSWORD_HASHERS
setting. This is a list of hashing algorithm
classes that this Django installation supports.
For storing passwords, Django will use the first hasher in
PASSWORD_HASHERS
. To store new passwords with a different algorithm,
put your preferred algorithm first in PASSWORD_HASHERS
.
For verifying passwords, Django will find the hasher in the list that matches
the algorithm name in the stored password. If a stored password names an
algorithm not found in PASSWORD_HASHERS
, trying to verify it will
raise ValueError
.
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.ScryptPasswordHasher",
]
这意味着 Django 除了使用 PBKDF2 来存储所有密码,也支持使用 PBKDF2SHA1 、argon2 和 bcrypt 来检测已存储的密码。
接下来的部分描述了高级用户修改这个配置的几个常见方法。
Argon2 is the winner of the 2015 Password Hashing Competition, a community organized open competition to select a next generation hashing algorithm. It's designed not to be easier to compute on custom hardware than it is to compute on an ordinary CPU. The default variant for the Argon2 password hasher is Argon2id.
Argon2 并不是 Django 的默认首选,因为它依赖第三方库。尽管哈希密码竞赛主办方建议立即使用 Argon2 ,而不是 Django 提供的其他算法。
To use Argon2id as your default storage algorithm, do the following:
Install the argon2-cffi package. This can be done by running
python -m pip install django[argon2]
, which is equivalent to
python -m pip install argon2-cffi
(along with any version requirement
from Django's setup.cfg
).
修改 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.ScryptPasswordHasher",
]
如果你需要 Django 升级密码( upgrade passwords ),请保留或添加这个列表中的任何条目。
bcrypt
¶Bcrypt 是一个非常流行的密码存储算法,尤其是为长期密码存储设计。Django 默认不使用它,因为它需要使用第三方库,但由于很多人想使用它,Django 只需要很少的努力就能支持 bcrypt 。
使用 Bcrypt 作为你的默认存储算法,需要以下步骤:
Install the bcrypt package. This can be done by running
python -m pip install django[bcrypt]
, which is equivalent to
python -m pip install bcrypt
(along with any version requirement from
Django's setup.cfg
).
修改 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.contrib.auth.hashers.ScryptPasswordHasher",
]
如果你需要 Django 升级密码( upgrade passwords ),请保留或添加这个列表中的任何条目。
现在 Django 将使用 Bcrypt 作为默认存储算法。
scrypt
¶scrypt is similar to PBKDF2 and bcrypt in utilizing a set number of iterations to slow down brute-force attacks. However, because PBKDF2 and bcrypt do not require a lot of memory, attackers with sufficient resources can launch large-scale parallel attacks in order to speed up the attacking process. scrypt is specifically designed to use more memory compared to other password-based key derivation functions in order to limit the amount of parallelism an attacker can use, see RFC 7914 for more details.
使用 scrypt 作为你的默认存储算法,需要以下步骤:
Modify PASSWORD_HASHERS
to list ScryptPasswordHasher
first.
That is, in your settings file:
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.ScryptPasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
]
如果你需要 Django 升级密码( upgrade passwords ),请保留或添加这个列表中的任何条目。
备注
scrypt
需要 OpenSSL 1.1及更高版本。
大多数密码哈希值包括一个盐,与他们的密码哈希值一起,以防止彩虹表攻击。盐本身是一个随机值,它增加了彩虹表的大小和成本,目前在 BasePasswordHasher
中的 salt_entropy
值设置为 128 比特。随着计算和存储成本的降低,这个值应该被提高。当实现你自己的密码散列器时,你可以自由地覆盖这个值,以便为你的密码散列器使用一个理想的熵值。salt_entropy
是以比特为单位。
实现细节
由于盐值的存储方法,salt_entropy
值实际上是一个最小值。例如,一个 128 的值将提供一个实际包含 131 位熵的盐。
The PBKDF2 and bcrypt algorithms use a number of iterations or rounds of
hashing. This deliberately slows down attackers, making attacks against hashed
passwords harder. However, as computing power increases, the number of
iterations needs to be increased. We've chosen a reasonable default (and will
increase it with each release of Django), but you may wish to tune it up or
down, depending on your security needs and available processing power. To do so,
you'll subclass the appropriate algorithm and override the iterations
parameter (use the rounds
parameter when subclassing a bcrypt hasher). For
example, to increase the number of iterations used by the default PBKDF2
algorithm:
Create a subclass of 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
里。
在 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.ScryptPasswordHasher",
]
现在 Django 使用 PBKDF2 存储密码时将会多次迭代。
备注
bcrypt rounds
is a logarithmic work factor, e.g. 12 rounds means
2 ** 12
iterations.
Argon2 has the following attributes that can be customized:
time_cost
控制哈希的次数。memory_cost
控制被用来计算哈希时的内存大小。parallelism
控制并行计算哈希的 CPU 数量。这三个属性的默认值足够适合你。如果你确定密码哈希过快或过慢,可以按如下方式调整它:
parallelism
你可以节省计算哈希的线程数。memory_cost
你可以节省内存的 KiB 。time_cost
和估计哈希一个密码所需的时间。挑选出你可以接受的 time_cost
。如果设置为1的 time_cost
慢的无法接受,则调低 memory_cost
。memory_cost
说明
argon2 命令行工具和一些其他的库解释了 memory_cost
参数不同于 Django 使用的值。换算公式是``memory_cost == 2 ** memory_cost_commandline`` 。
scrypt
¶scrypt has the following attributes that can be customized:
work_factor
控制哈希的次数。block_size
parallelism
controls how many threads will run in parallel.maxmem
limits the maximum size of memory that can be used during the
computation of the hash. Defaults to 0
, which means the default
limitation from the OpenSSL library.We've chosen reasonable defaults, but you may wish to tune it up or down, depending on your security needs and available processing power.
Estimating memory usage
scrypt_的最低内存需求是:
work_factor * 2 * block_size * 64
so you may need to tweak maxmem
when changing the work_factor
or
block_size
values.
当用户登录时,如果用户的密码使用首选算法以外的算法保存,Django 会自动升级这个算法成为首选算法。这意味着旧的 Django 安装会在用户登录时自动得到更多的安全,并且当它们创建时你可以切换到新的更好的存储算法。
然而,Django 只会使用 PASSWORD_HASHERS
提到的算法升级密码,因此当你升级到新系统时你要确保你从没有删除过这个列表的条目。如果你删除过,那么使用的没有列出的算法的用户将不会升级。当增加(或减少) PBKDF2 迭代的次数、bcrypt 的轮次或者 argon2 属性,哈希过的密码将被更新。
注意,如果数据库内的所有密码没有在默认哈希算法里编码,则由于非默认算法的密码编码的用户登录请求持续时间和不存在用户(运行过默认哈希)的登录请求持续时间的不同,你可能会受到用户枚举时间攻击。你可以使用升级旧密码的哈希值来缓解此问题。
If you have an existing database with an older, weak hash such as MD5, you might want to upgrade those hashes yourself instead of waiting for the upgrade to happen when a user logs in (which may never happen if a user doesn't return to your site). In this case, you can use a "wrapped" password hasher.
For this example, we'll migrate a collection of MD5 hashes to use
PBKDF2(MD5(password)) and add the corresponding password hasher for checking
if a user entered the correct password on login. We assume we're using the
built-in User
model and that our project has an accounts
app. You can
modify the pattern to work with any algorithm or with a custom user model.
首先,我们添加一个自定义的哈希:
from django.contrib.auth.hashers import (
PBKDF2PasswordHasher,
MD5PasswordHasher,
)
class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
algorithm = "pbkdf2_wrapped_md5"
def encode_md5_hash(self, md5_hash, salt, iterations=None):
return super().encode(md5_hash, salt, iterations)
def encode(self, password, salt, iterations=None):
_, _, md5_hash = MD5PasswordHasher().encode(password, salt).split("$", 2)
return self.encode_md5_hash(md5_hash, salt, iterations)
数据迁移可能类似于这样:
from django.db import migrations
from ..hashers import PBKDF2WrappedMD5PasswordHasher
def forwards_func(apps, schema_editor):
User = apps.get_model("auth", "User")
users = User.objects.filter(password__startswith="md5$")
hasher = PBKDF2WrappedMD5PasswordHasher()
for user in users:
algorithm, salt, md5_hash = user.password.split("$", 2)
user.password = hasher.encode_md5_hash(md5_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
中添加配置:
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"accounts.hashers.PBKDF2WrappedMD5PasswordHasher",
]
包含你的站点使用的此列表中的其他算法。
在 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.ScryptPasswordHasher",
"django.contrib.auth.hashers.MD5PasswordHasher",
]
相应的算法名是:
pbkdf2_sha256
pbkdf2_sha1
argon2
bcrypt_sha256
bcrypt
scrypt
md5
如果你编写自己的密码哈希包含工作因子,比如迭代数量。你应该实现一个 harden_runtime(self, password, encoded)
方法来消除编码密码时提供的工作因子和默认的哈希工作因子之间的运行时间差。这样可以防止用户枚举时间攻击,因为旧的迭代次数中对密码编码的用户与不存在的用户(运行默认哈希的默认迭代次数)在登录时存在差异。
以 PDKDF2 为例,如果编码包含20000次迭代,并且默认哈希迭代是30000,那么该方法应该通过另外的10000次迭代的 PBKDF2 运行密码。
如果你的哈希没有工作因子,可以将该方法实现为 no-op (pass) 。
The django.contrib.auth.hashers
module provides a set of functions
to create and validate hashed passwords. You can use them independently
from the User
model.
check_password
(password, encoded, setter=None, preferred='default')¶If you'd like to manually authenticate a user by comparing a plain-text
password to the hashed password in the database, use the convenience
function check_password()
. It takes two mandatory arguments: the
plain-text password to check, and the full value of a user's password
field in the database to check against. It returns True
if they match,
False
otherwise. Optionally, you can pass a callable setter
that
takes the password and will be called when you need to regenerate it. You
can also pass preferred
to change a hashing algorithm if you don't want
to use the default (first entry of PASSWORD_HASHERS
setting). See
已包含的哈希 for the algorithm name of each hasher.
make_password
(password, salt=None, hasher='default')¶通过此应用的格式创建一个哈希密码。它需要一个必需的参数:纯文本密码(字符串或字节)。或者,如果你不想使用默认配置( PASSWORD_HASHERS
配置的首个条目 ),那么可以提供 salt 和 使用的哈希算法。有关每个哈希的算法名,可查看 已包含的哈希 。如果密码参数是 None
,将返回一个不可用的密码(永远不会被 check_password()
通过的密码)。
is_password_usable
(encoded_password)¶如果密码是 User.set_unusable_password()
的结果,则返回 False
。
用户经常会选择弱密码。为了缓解这个问题,Django 提供可插拔的密码验证。你可以同时配置多个密码验证。Django 已经包含了一些验证,但你也可以编写你自己的验证。
每个密码验证器必须提供给用户提供帮助文案以向用户解释要求,验证密码并在不符合要求时返回错误信息,并且可选择接受已经设置过的密码。验证器也可以使用可选设置来微调它们的行为。
验证由 AUTH_PASSWORD_VALIDATORS
控制。默认的设置是一个空列表,这意味着默认是不验证的。在使用默认的 startproject
创建的新项目中,默认启用了验证器集合。
默认情况下,验证器在重置或修改密码的表单中使用,也可以在 createsuperuser
和 changepassword
命令中使用。验证器不能应用在模型层,比如 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
检查密码是否在常用密码列表中。默认情况下,它会与列表中的2000个常用密码作比较。NumericPasswordValidator
检查密码是否是完全是数字的。对于 UserAttributeSimilarityValidator
和 CommonPasswordValidator
,我们在这个例子里使用默认配置。NumericPasswordValidator
不需要设置。
帮助文本和来自密码验证器的任何错误信息始终按照 AUTH_PASSWORD_VALIDATORS
列出的顺序返回。
Django 包含了四种验证器:
MinimumLengthValidator
(min_length=8)¶Validates that the password is of a minimum length.
The minimum length can be customized with the min_length
parameter.
UserAttributeSimilarityValidator
(user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7)¶Validates that the password is sufficiently different from certain attributes of the user.
user_attributes
参数应该是可比较的用户属性名的可迭代参数。如果没有提供这个参数,默认使用:'username', 'first_name', 'last_name', 'email'
。不存在的属性会被忽略。
The maximum allowed similarity of passwords can be set on a scale of 0.1
to 1.0 with the max_similarity
parameter. This is compared to the
result of difflib.SequenceMatcher.quick_ratio()
. A value of 0.1
rejects passwords unless they are substantially different from the
user_attributes
, whereas a value of 1.0 rejects only passwords that are
identical to an attribute's value.
The max_similarity
parameter was limited to a minimum value of 0.1.
CommonPasswordValidator
(password_list_path=DEFAULT_PASSWORD_LIST_PATH)¶Validates that the password is not a common password. This converts the password to lowercase (to do a case-insensitive comparison) and checks it against a list of 20,000 common password created by Royce Williams.
password_list_path
用来设置自定义的常用密码列表文件的路径。这个文件应该每行包含一个小写密码,并且文件是纯文本或 gzip 压缩过的。
The list of 20,000 common passwords was updated to the most recent version.
NumericPasswordValidator
¶Validate that the password is not entirely numeric.
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
定义的验证器,但通过一个验证器替代集合来调用此函数,然后向其他函数传递的密码验证器参数传递结果,将使用你自定义的验证器集合。当你有一个应用于大多数场景的通用的验证器集合时,需要一个自定义的集合来用于特殊情况。当你始终使用同一个验证器集合时,则不需要这个函数,因为默认使用是 AUTH_PASSWORD_VALIDATORS
的配置。
validator_config
的结构和 AUTH_PASSWORD_VALIDATORS
的结构相同。这个函数的返回值可以传递给上述函数列表的``password_validators`` 参数。
注意,如果将密码传递给其中一个函数,应该始终是明文密码,而不是哈希过的密码。
如果 Django 内置的验证器不满足你的需求,你可以编写自定义的验证器。验证器的接口很小。它们必须实现两个方法:
validate(self, password, user=None)
:验证密码。如果密码有效,返回 None
,否则引发 ValidationError
错误。你必须能够处理 user
为 None
的情况,如果这样会让验证器无法运行,只需返回 None
即可。get_help_text()
:提供一个帮助文本向用户解释密码要求。验证器的 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)
,在密码修改成功后调用。比如说用来防止密码重用。但是,如果你决定存储用户之前的密码,则不应该以明文形式存储。
9月 22, 2023