Django 密码哈希器使用 php 格式的函数 password_hash()



我必须添加一个向后兼容的 Django 应用程序,该应用程序支持在使用 PHP 函数创建的数据库中保留的遗留密码password_hash()其输出类似于

$2y$10$puZfZbp0UGMYeUiyZjdfB.4RN9frEMy8ENpih9.jOEngy1FJWUAHy

(盐渍河豚地穴算法,10轮哈希

(Django 支持带有算法前缀名称的格式,因此如果我使用BCryptPasswordHasher作为主哈希器输出,如下所示:

bcrypt$$2y$10$puZfZbp0UGMYeUiyZjdfB.4RN9frEMy8ENpih9.jOEngy1FJWUAHy

我已经创建了自定义的BCryptPasswordHaser,如下所示:

class BCryptPasswordHasher(BasePasswordHasher):
algorithm = "bcrypt_php"
library = ("bcrypt", "bcrypt")
rounds = 10
def salt(self):
bcrypt = self._load_library()
return bcrypt.gensalt(self.rounds)
def encode(self, password, salt):
bcrypt = self._load_library()
password = password.encode()
data = bcrypt.hashpw(password, salt)
return f"{data.decode('ascii')}"
def verify(self, incoming_password, encoded_db_password):
algorithm, data = encoded_db_password.split('$', 1)
assert algorithm == self.algorithm
db_password_salt = data.encode('ascii')
encoded_incoming_password = self.encode(incoming_password, db_password_salt)
# Compare of `data` should only be done because in database we don't persist alg prefix like `bcrypt$`
return constant_time_compare(data, encoded_incoming_password)
def safe_summary(self, encoded):
empty, algostr, work_factor, data = encoded.split('$', 3)
salt, checksum = data[:22], data[22:]
return OrderedDict([
('algorithm', self.algorithm),
('work factor', work_factor),
('salt', mask_hash(salt)),
('checksum', mask_hash(checksum)),
])
def must_update(self, encoded):
return False
def harden_runtime(self, password, encoded):
data = encoded.split('$')
salt = data[:29]  # Length of the salt in bcrypt.
rounds = data.split('$')[2]
# work factor is logarithmic, adding one doubles the load.
diff = 2 ** (self.rounds - int(rounds)) - 1
while diff > 0:
self.encode(password, salt.encode('ascii'))
diff -= 1

AUTH_USER_MODEL像:

from django.contrib.auth.hashers import check_password
from django.db import models

class User(models.Model):
id = models.BigAutoField(primary_key=True)
email = models.EmailField(unique=True)
password = models.CharField(max_length=120, blank=True, null=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
EMAIL_FIELD = 'email'
def check_password(self, raw_password):
def setter():
pass
alg_prefix = "bcrypt_php$"
password_with_alg_prefix = alg_prefix + self.password
return check_password(raw_password, password_with_alg_prefix, setter)

设置base.py

...
AUTH_USER_MODEL = 'custom.User'
PASSWORD_HASHERS = [
'custom.auth.hashers.BCryptPasswordHasher',
]
...

在这种情况下,在验证密码之前,我添加bcrypt$前缀,然后进行验证,但在数据库中,密码保留而不bcrypt$

它有效,但我想知道是否有其他更简单的方法可以做到这一点,或者也许有人遇到了同样的问题?

我想补充一点,PHP 应用程序和新的 Django 都应该支持这两种格式,我无法对旧版 PHP 进行更改。更改只能在新的 Django 服务器上完成。

可以使用自定义身份验证后端。 首先在身份验证应用中创建一个文件(假设您将其命名为 auth(,调用该文件 backends.py

backends.py 内容

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import BaseBackend
UserModel = get_user_model()

class ModelBackend(BaseBackend):
"""
Authenticate against the settings ADMIN_LOGIN and ADMIN_PASSWORD.
Use the login name and a hash of the password. For example:
ADMIN_LOGIN = 'admin'
ADMIN_PASSWORD = 'pbkdf2_sha256$30000$Vo0VlMnkR4Bk$qEvtdyZRWTcOsCnI/oQ7fVOu1XAURIZYoOZ3iq8Dr4M='
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
if username is None or password is None:
return
try:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
UserModel().set_password(password)
else:
if user.check_password(password):
# user exists
return user

然后在您的 settings.py 将您的 backends.py 包含在AUTHENTICATION_BACKENDS变量中

它应该看起来像这样

AUTHENTICATION_BACKENDS = [
"djangoprojectname.auth.backends.ModelBackend",
"django.contrib.auth.backends.ModelBackend",
]

最后一点是在您的身份验证模型中实现 check_password 方法,基于我们这里的示例,我们在身份验证应用程序中打开文件 models.py,在类 User 中添加了基于 bcrypt 算法实现检查密码的方法。

import bcrypt
class User(AbstractUser):
first_name = models.CharField(max_length=255, null=True)
email = models.CharField(unique=True, max_length=255, blank=True, null=True)
def check_password(self, raw_password):
def setter():
pass
check = bcrypt.checkpw(bytes(raw_password, 'utf-8'), bytes(self.password, 'utf-8'))
return check
def set_password(self, raw_password):
hashed = bcrypt.hashpw(bytes(raw_password, 'utf-8'), bcrypt.gensalt(rounds=10))
encrypted = str(hashed, 'UTF-8')
self.password = encrypted
self._password = encrypted 

然后,在您的登录视图中,您可能必须在身份验证应用程序中的 views.py 中实现这样的东西

username = data.get("username")
password = data.get("password")
if username is None or password is None:
pass # implement for requesting username and password
user = authenticate(username=username, password=password)
if user is None:
pass # implement for invalid credentials
# check user confirmation
confirmed = getattr(user, 'confirmed', None)
if confirmed is False or None:
pass # implement for user not confirmed
# check if user is active
isactive = getattr(user, 'isactive', None)
if isactive is False or None:
pass # implement for user account disabled
login(request, user)
# return view or json response or whatever for login success

解决使用 PHPpassword_hash(password, PASSWORD_DEFAULT)生成的哈希的密码检查的反向任务

from django.contrib.auth.hashers import check_password
check_password(decoded_pass, 'bcrypt${0}'.format(php_hash), preferred='bcrypt')

为我工作。

Django 2.2测试。bcrypt$前缀黑客基于另一个 SO 答案。

最新更新