Допустим что сайт, которым пользуются ваши пользователи, написан на 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 проекта.
-
прочитайте, как модифицировать систему авторизации Django
-
Зарегистрируйте новый бэкенд (еще не существующий) в
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', ]
-
Создайте бэкенд авторизации пользователей 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 и не очень очевидна, поэтому если вы не сделаете тесты/документацию сейчас, поддержка проекта в будущем усложнится.
Автор: AcckiyGerman