Django 哈希与旧数据库集成

问题描述

我在 MysqL数据库上使用 django。我已经集成了除密码之外的所有内容。旧数据库以这种方式存储密码 $2a$10$Pdg3h8AVZ6Vl3X1mMKgQDuMriv8iysnValEa5YZO3j9pEboLrOBUK 和 django 仅在相同的哈希具有 bcrypt$ 前缀 bcrypt$$2a$10$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,obj=None):
        return set()

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

    def has_perm(self,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,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$$2a$10$rA59QU2GsWR4v6hugdYhruxY0bgZYVLv6ncxRe3BiDJMEpK0A0huW'
>>> check_password("111111",ps_bc)
True
>>> check_password("111111",ps)
False
>>> user.check_password("111111") 
False

解决方法

Django 首先指定使用的 哈希 算法,因为它可以例如为每个用户使用不同的哈希算法。

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

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

方案一:更新密码

您可以通过以下方式批量更新记录:

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

MyModel.objects.update(
    password=Concat(Value('bcrypt$'),F('password'))
)

这将因此通过在散列前添加 'bcrypt$' 来更新密码字段(如果密码存储在另一个字段中,则使用该字段名称)。

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

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

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

然后我们可以在 AUTHENTICATION_BACKENDS [Django-doc] 中注册这个后端:

# settings.py

# …

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

# …