04.03.2019       Выпуск 272 (04.03.2019 - 10.03.2019)       Статьи

Использование учетных записей Joomla в проекте на Django

Допустим что сайт, которым пользуются ваши пользователи, написан на Joomla, но для создания нового продукта для вашей аудитории вы выбрали связку Python/Django. Как следствие, возникает необходимость использовать в Django учетные записи пользователей из базы данных Joomla. Проблема однако в том, что Joomla и Django используют разные алгоритмы хэширования паролей, поэтому просто скопировать учетные записи не получится. Почитав документацию Django, stack overflow и потратив некоторое время, получилось нижеописанное решение, которое по максимуму использует рекомендуемые практики разработки под Django. 

Читать>>




Экспериментальная функция:

Ниже вы видите текст статьи по ссылке. По нему можно быстро понять ссылка достойна прочтения или нет

Просим обратить внимание, что текст по ссылке и здесь может не совпадать.

Допустим что сайт, которым пользуются ваши пользователи, написан на Joomla, но для создания нового продукта для вашей аудитории вы выбрали связку Python/Django.

Как следствие, возникает необходимость использовать в Django учетные записи пользователей из базы данных Joomla.

Проблема однако в том, что Joomla и Django используют разные алгоритмы хэширования паролей, поэтому просто скопировать учетные записи не получится.

Почитав документацию Django, stack overflow и потратив некоторое время, получилось нижеописанное решение, которое по максимуму использует рекомендуемые практики разработки под Django.

Предупреждения

Данное архитектурное решение может вам не подойти, см. обсуждение в комментариях.

Чтобы понимать, что происходит в нижеприведенных примерах, вы должны обладать некоторым пониманием архитектуры Django.

Также я предполагаю, что вы знаете, как развернуть Django проект, поэтому не описываю этот процесс.

Код скопирован из рабочего проекта но его легко будет подстроить под ваш проект с минимумом изменений.

Вероятно, в следующей мажорной версии Django данный код может поломаться, однако сам принцип решения останется тем же самым.

В данном руководстве я не описываю фронтэнд системы авторизации, так как:

  • какой у вас будет front-end — зависит от нужд вашего проекта (это может вообще быть Json API endpoint, например)
  • эта информация уже описана в официальных руководствах Django и разнообразных статьях для начинающих

Алгоритм

  • подключить базу данных (БД) Joomla к проекту Django
  • создать модель "JoomlaUser", представляющую пользователя из БД Joomla
  • написать функцию check_joomla_password(), проверяющую, что введенный пароль совпадает с оригинальным паролем пользователя.
  • добавить в проект новый бекенд авторизации "Joomla Auth Backend", который, при авторизации клиента в Django, будет доставать учетную запись пользователя из БД Joomla

1. Подключение к БД Joomla:

  • Прочитайте, как Django работает с несколькими базами данных
  • для подключения базы данных Joomla в наш Django проект, добавьте следующий код в файл с настройками проекта /project_name/settings.py:


    DATABASES = {
    # БД по умолчанию 
    'default': {
        ...
    },
    
    'joomla_db': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {},
        'NAME': 'joomla_database_name',
        # Don't store passwords in the code, instead use env vars:
        'USER':     os.environ['joomla_db_user'],
        'PASSWORD': os.environ['joomla_db_pass'],
        'HOST': 'joomla_db_host, can be localhost or remote IP',
        'PORT': '3306',
    }
    }



При необходимости, в этом же файле с настройками проекта, можно включить логирование запросов к БД:

# add logging to see DB requests:
LOGGING = {
    'version': 1,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['console'],
        },
    },
}

2. создайте модель JoomlaUser

  • Прочитайте, как модель Django может использовать существующую БД
  • Подумайте, где расположить новую модель "JoomlaUser".
    В моем проекте я создал application с именем "users" (manage.py startapp users). В ней будет лежать бекэнд авторизации и модель пользователя Joomla
  • сгенерируем модель автоматически, используя inspectdb:
    python manage.py inspectdb live_users --database="joomla_db"
    joomla_db — название БД, которое вы указали в settings.py/DATABASES;
    live_users — название таблицы c учетными записями.




  • добавьте вашу модель в users/models.py:


    class JoomlaUser(models.Model):
    """ Represents our customer from the legacy Joomla database. """
    
    username = models.CharField(max_length=150, primary_key=True)
    email = models.CharField(max_length=100)
    password = models.CharField(max_length=100)
    # you can copy more fields from `inspectdb` output, 
    # but it's enough for the example
    
    class Meta:
        # joomla db user table. WARNING, your case can differs.
        db_table = 'live_users'
        # readonly 
        managed = False
        # tip for the database router
        app_label = "joomla_users"  



Далее нам необходимо убедиться, что модель будет обращатся к правильной БД. Для этого добавим в проект роутер для запросов к разным БД, который будет перенаправлять запросы от модели JoomlaUser к её родной базе данных.

  1. Создайте файл "db_routers.py" в основной папке проекта (там же, где лежит ваш "settings.py"):


    # project_name/db_routers.py
    class DbRouter:
    """this router makes sure that django uses legacy 'Joomla' database for models,
    that are stored there (JoomlaUser)"""
    
    def db_for_read(self, model, **kwargs):
        if model._meta.app_label == 'joomla_user':
            return 'joomla_db'
        return None
    
    def db_for_write(self, model, **kwargs):
        if model._meta.app_label == 'joomla_user':
            return 'joomla_db'
        return None



  2. зарегистрируйте новый роутер в settings.py:


    # ensure that Joomla users are populated from the right database:
    DATABASE_ROUTERS = ['project_name.db_routers.DbRouter']



Теперь вы можете достать учетную запись из старой БД.
Запустите терминал Django и попробуйте вытащить существующего пользователя: python manage.py shell

>>> from users.models import JoomlaUser
>>> print(JoomlaUser.objects.get(username='someuser'))
JoomlaUser object (someusername)
>>> 

Если все работает (вы видите пользователя), то переходим к следующему шагу. Иначе смотрите на вывод ошибок и исправляйте настройки.

3. Проверка пароля учетной записи Joomla

Joomla не хранит пароли пользователей, но их хэш, например
$2y$10$aoZ4/bA7pe.QvjTU0R5.IeFGYrGag/THGvgKpoTk6bTz6XNkY0F2e

Начиная с версии Joomla v3.2, пароли пользователей зашифрованы с помощью алгоритма BLOWFISH.

Так что я загрузил python код с этим алгоритмом:

pip install bcrypt
echo bcrypt >> requirements.txt

И создал функцию для проверки паролей в файле users/backend.py:

def check_joomla_password(password, hashed):
    """
    Check if password matches the hashed password,
    using same hashing method (Blowfish) as Joomla >= 3.2

    If you get wrong results with this function, check that
    the Hash starts from prefix "$2y", otherwise it is 
    probably not a blowfish hash

    :return: True/False
    """
    import bcrypt
    if password is None:
        return False
    # bcrypt requires byte strings
    password = password.encode('utf-8')
    hashed = hashed.encode('utf-8')

    return hashed == bcrypt.hashpw(password, hashed)

Внимание! Joomla версии ниже чем 3.2 использует другой метод хеширования (md5+salt), так что эта функция не будет работать. В таком случае почитайте
обсуждение на Stackoverflow и создайте функцию для проверки хэша, которая будет выглядеть примерно так:

# WARNING - THIS FUNCTION WAS NOT TESTED WITH REAL JOOMLA USERS
# and definitely has some errors
def check_old_joomla_password(password, hashed):
    from hashlib import md5
    password = password.encode('utf-8')
    hashed = hashed.encode('utf-8')
    if password is None:
        return False

    # check carefully this part:
    hash, salt = hashed.split(':')
    return hash == md5(password+salt).hexdigest()

К сожалению у меня сейчас нет под рукой базы пользователей из старой версии Joomla, поэтому я не смогу протестировать эту функцию для вас.

4. Бэкенд авторизации пользователей Joomla

Теперь вы готовы создать Django бэкенд для авторизации пользователей из Joomla проекта.

  1. прочитайте, как модифицировать систему авторизации Django



  2. Зарегистрируйте новый бэкенд (еще не существующий) в project/settings.py:


    AUTHENTICATION_BACKENDS = [
    # Check if user already in the local DB
    # by using default django users backend
    'django.contrib.auth.backends.ModelBackend',
    
    # If user was not found among django users,
    # use Joomla backend, which:
    #   - search for user in Joomla DB
    #   - check joomla user password
    #   - copy joomla user into Django user.
    'users.backend.JoomlaBackend',
    ]



  3. Создайте бэкенд авторизации пользователей Joomla в users/backend.py



from django.contrib.auth.models import User
from .models import JoomlaUser

def check_joomla_password(password, hashed):
    # this is a fuction, that we wrote before
    ...

class JoomlaBackend:
    """ authorize users against Joomla user records """
    def authenticate(self, request, username=None, password=None):
        """
        IF joomla user exists AND password is correct:
            create django user
            return user object 
        ELSE:
            return None
        """
        try:
            joomla_user = JoomlaUser.objects.get(username=username)
        except JoomlaUser.DoesNotExist:
            return None
        if check_joomla_password(password, joomla_user.password):
            # Password is correct, let's create and return Django user,
            # identical to Joomla user:

            # but before let's ensure there is no same username
            # in DB. That could happen, when user changed password
            # in Joomla, but Django doesn't know that
            User.objects.filter(username=username).delete()  

            return User.objects.create_user(
                username=username,
                email=joomla_user.email,
                password=password,
                # any additional fields from the Joomla user:
                ...
            )

    # this method is required to match Django Auth Backend interface
    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

Итог

Поздравляю — теперь пользователи вашего существующего Joomla сайта могут использовать свои учётные данные на новом сайте/приложении.

По мере авторизации активных пользователей через новый интерфейс, они будут по одному скопированы в новую базу данных.

Как вариант, вы можете не захотеть копировать сущности пользователей из старой системы в новую.

В таком случае вот вам ссылка на статью, в которой описывается, как в Django заменить модель пользователя по умолчанию на свою (вышеописанную модель JoomlaUser).

Конечное решение, переносить или не переносить пользователей, принимайте на основе того, в каких взаимоотношениях будут новый и старый проект. Например, где будет происходить регистрация новых пользователей, какой сайт/приложение будет основным, и т.д.

Тестирование и документация

Теперь пожалуйста добавьте соответствующие тесты и документацию, покрывающие новый код. Логика данного решения тесно переплетена с архитектурой Django и не очень очевидна, поэтому если вы не сделаете тесты/документацию сейчас, поддержка проекта в будущем усложнится.



Лучшая Python рассылка




Разместим вашу рекламу

Пиши: mail@pythondigest.ru

Нашли опечатку?

Выделите фрагмент и отправьте нажатием Ctrl+Enter.

Система Orphus