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

habr.png

Допустим что сайт, которым пользуются ваши пользователи, написан на 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',
    }
    }
    

Далее, в этом же файле, добавляем роутер для БД, который будет перенаправлять запросы от модели JoomlaUser к правильной БД:

# ensure that Joomla users are populated from the right database:
DATABASE_ROUTERS = ['manager.router.DatabaseAppsRouter']
DATABASE_APPS_MAPPING = {'joomla_users': 'joomla_db'}

# you also can create your own database router:
# https://docs.djangoproject.com/en/dev/topics/db/multi-db/#automatic-database-routing

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

# 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
  • запустите python manage.py inspectdb --database="joomla_db" и исследуйте существующую базу данных Joomla.
  • в частности нас интересует какие поля содержит таблица учетных записей и их репрезентация в виде полей модели Django
  • добавьте вашу модель в 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, see "settings.DATABASE_APPS_MAPPING"
        app_label = "joomla_users"  
    

Запускайте терминал 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 и не очень очевидна, поэтому если вы не сделаете тесты/документацию сейчас, поддержка проекта в будущем усложнится.

© Habrahabr.ru