Django 哈希与遗留数据库集成

Django hash integrate with legacy database

我在 mysql 遗留数据库上使用 django。我已经集成了除密码以外的所有内容。遗留数据库以这种方式存储密码 a$Pdg3h8AVZ6Vl3X1mMKgQDuMriv8iysnValEa5YZO3j9pEboLrOBUK 并且 django 仅在相同的散列具有 bcrypt$ 前缀 bcrypt$a$Pdg3h8AVZ6Vl3X1mMKgQDuMriv8iysnValEa5YZO3j9pEboLrOBUK 时才读取。如何让 Django 通过读取第一个示例哈希来验证用户?

为什么 django 在密码中添加前缀?

更新 我添加了模型后端

from django.contrib.auth.backends import ModelBackend
from users.models import TbUser
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.db.models import Exists, OuterRef, Q

UserModel = get_user_model()


class BaseBackend:
    def authenticate(self, request, **kwargs):
        return None

    def get_user(self, user_id):
        return None

    def get_user_permissions(self, user_obj, obj=None):
        return set()

    def get_group_permissions(self, user_obj, obj=None):
        return set()

    def get_all_permissions(self, user_obj, obj=None):
        return {
            *self.get_user_permissions(user_obj, obj=obj),
            *self.get_group_permissions(user_obj, obj=obj),
        }

    def has_perm(self, user_obj, perm, obj=None):
        return perm in self.get_all_permissions(user_obj, obj=obj)


class ModelBackend(BaseBackend):
    """
    Authenticates against settings.AUTH_USER_MODEL.
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        print(">>>>>>>>>>>>>>>>>> Authentication start")
        print("User: ", username, " Password: ", password)
        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.password.startswith('bcrypt'):
                u = user
            else:
                u = UserModel(password=f'bcrypt${user.password}')
                print(">>>>>>>>>>>>>>>>>> Authentication prefix adding...")
                print(">>>>>>>>>>>>>>>>>> NO-PREFIX: ", user.password)
                print(">>>>>>>>>>>>>>>>>> WITH-PREFIX: ",
                      f'bcrypt${user.password}')
                print(">>>>>>>>>>>>>>>>>> CHECKING PASSWORD NO-PREFIX: ",
                      user.check_password(password))
                print(">>>>>>>>>>>>>>>>>> CHECKING PASSWORD WITH-PREFIX: ",
                      user.check_password(f'bcrypt${user.password}'))
                #user = UserModel(password=f'bcrypt${user.password}')
            if u.check_password(password) and self.user_can_authenticate(user):
                return user

    def user_can_authenticate(self, user):
        """
        Reject users with is_active=False. Custom user models that don't have
        that attribute are allowed.
        """
        is_active = getattr(user, 'is_active', None)
        return is_active or is_active is None

settings.py 正在使用此身份验证后端

AUTHENTICATION_BACKENDS = [
    'users.backends.ModelBackend',
    'django.contrib.auth.backends.ModelBackend'
]

当 django 检查密码时,它会抛出以下错误

File "\site-packages\django\contrib\auth\__init__.py", line 73, in authenticate
    user = backend.authenticate(request, **credentials)
  File "\auth\users\backends.py", line 74, in authenticate
    if user.check_password(password) and self.user_can_authenticate(user):
  File "\site-packages\django\contrib\auth\base_user.py", line 112, in check_password
    return check_password(raw_password, self.password, setter)
  File "\site-packages\django\contrib\auth\hashers.py", line 49, in check_password
    must_update = hasher_changed or preferred.must_update(encoded)
  File "\site-packages\django\contrib\auth\hashers.py", line 443, in must_update
    return int(rounds) != self.rounds
ValueError: invalid literal for int() with base 10: '2b'

我已经使用了这个实现的一部分https://github.com/django/django/blob/main/django/contrib/auth/backends.py

测试

>>> from users.models import TbUser
>>> from django.contrib.auth import check_password
>>> user = TbUser.objects.get(username="Gastro")
>>> user
<TbUser: Gastro>
>>> ps = user.password
>>> ps_bc = "bcrypt$" + ps
>>> ps_bc
'bcrypt$a$rA59QU2GsWR4v6hugdYhruxY0bgZYVLv6ncxRe3BiDJMEpK0A0huW'
>>> check_password("111111", ps_bc)
True
>>> check_password("111111", ps)
False
>>> user.check_password("111111") 
False

Django 首先指定使用的 散列 算法,因为它可以为每个用户使用不同的散列算法。

基本上有两种方法可以解决这个问题:

  1. 更改数据库中的密码;或
  2. 制作一个(稍微)不同的身份验证后端,它将在其前面加上密码。

选项 1:更新密码

您可以批量更新记录:

from django.db.models import F, Value
from django.db.models.functions import Concat

MyModel.objects.update(
    <b>password=Concat(Value('bcrypt$'), F('password'))</b>
)

这将因此更新密码字段(如果密码存储在另一个字段中,则使用该字段名称),方法是在哈希值前添加 'bcrypt$'

选项 2:自定义身份验证方法

我们可以将 ModelBackend 子类化并稍微重写为:

# <i>app_name</i>/backends.py

django.contrib.auth.backends import ModelBackend

class MyModelBackend(ModelBackend):

    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.password.startswith('bcrypt'):
                u = user
            else:
                <b>u = UserModel(password=f'bcrypt_sha256${user.password}')</b>
            if u.check_password(password) and self.user_can_authenticate(user):
                return user

然后我们可以在 AUTHENTICATION_BACKENDS [Django-doc]:

中注册这个后端
# settings.py

# …

AUTHENTICATION_BACKENDS = [
    '<i>app_name</i>.backends.MyModelBackend',
    'django.contrib.auth.backends.ModelBackend'
]

# …