Django - Удаление поддоменов из моего приложения для мультитенансирования

У меня есть работающее приложение на Django с несколькими арендаторами и изолированными базами данных, которое в настоящее время использует поддомен для каждого арендатора и потоков. Префикс субдомена используется для установления соединения с соответствующей базой данных, так что:

  • client1.mysite.com доступ к базе данных client1
  • client2.mysite.com доступ к базе данных client2

И так далее. Вот текущий код, который я имею:

middleware:

import threading

from app.utils import tenant_db_from_the_request

Thread_Local = threading.local()


class AppMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        db = tenant_db_from_the_request(request)
        setattr(Thread_Local, 'DB', db)
        response = self.get_response(request)
        return response


def get_current_db_name():
    return getattr(Thread_Local, 'DB', None)

utils:

def hostname_from_the_request(request):
    return request.get_host().split(':')[0].lower()


def tenant_db_from_the_request(request):
    hostname = hostname_from_the_request(request)
    tenants_map = get_tenants_map()
    return tenants_map.get(hostname)


def get_tenants_map():
    return dict(Tenants.objects.values_list('subdomain', 'database',))

маршрутизаторы:

class AppRouter:

    def db_for_read(self, model, **hints):
        return get_current_db_name()

    def db_for_write(self, model, **hints):
        return get_current_db_name()

    def allow_relation(self, *args, **kwargs):
        return True

    def allow_syncdb(self, *args, **kwargs):
        return None

    def allow_migrate(self, *args, **kwargs):
        return None

Использование поддоменов не совсем идеально, поэтому я пытаюсь переключить префикс поддомена на суффикс, который будет добавлен к имени пользователя каждого арендатора. Таким образом, каждый пользователь будет обращаться к одному и тому же URL:

mysite.com

А суффикс будет добавлен к имени пользователя следующим образом:

  • user@client1 доступ к базе данных client1
  • user@client2 доступ к базе данных client2

Поскольку у меня уже есть возможность работать с субдоменом, я решил изменить только промежуточное ПО для получения суффикса имени пользователя и использовать сессию для хранения псевдонима арендатора, который будет использоваться при каждом запросе к базе данных. Исходя из этого, я изменил только мой файл utils следующим образом:

def hostname_from_the_request(request):
    # changed the request to get the 'db_alias' from the session
    return request.session.get('db_alias', None)


def get_tenants_map():
    # change the mapping to retrieve the database based on a alias, instead of subddomain
    return dict(Tenants.objects.values_list('alias', 'database',))

И добавил 3 строки кода перед процессом аутентификации в моем представлении логина:

if request.method == "POST":

    if form.is_valid():

        # retrieve the alias from the username
        complete_username = form.cleaned_data.get('username')
        db_alias = complete_username.split('@')[1]
        request.session['db_alias'] = db_alias

        user = authenticate(
            username=form.cleaned_data.get('username'),
            password=form.cleaned_data.get('password'),
        )

Это работает, но только после второй попытки входа. Первая всегда возвращает ошибку неудачной аутентификации. Сначала я подумал, что это происходит потому, что сессия создается только после неудачной попытки входа, но с помощью операторов печати и точек останова я могу подтвердить, что это не так. Сессия создается до аутентификации, и имя базы данных извлекается корректно (извлечение имени корректно, потому что после второй попытки вход в систему проходит успешно и каждый запрос обрабатывается на соответствующей базе данных).

Я пытался изменить настройки промежуточного ПО и поставить AppMiddleware сразу после промежуточного ПО сессии (раньше оно было последним в списке), но безрезультатно.

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

Я не очень хорошо разбираюсь в Python / Django, поэтому, возможно, я упускаю что-то очевидное. Или, может быть, это неправильный способ достичь того, что я хочу. Итак, я что-то упускаю? Как я могу заставить это работать при первой попытке входа?

Я переписал весь middleware и изменил его логику. Я полностью перестал использовать базу данных DomainModel из django-tenants, которая стала излишней в моем случае.

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

Я также полностью перестал использовать метод context_tenant из django_tenant, потому что он просто не отвечал на запросы. Я заменил его на self.connection.set_schema(schema_name), который работает очень хорошо в моем случае.

В промежуточном ПО я получаю идентификатор cookie пользователя, декодирую его и фильтрую соответствующую запись из базы данных UserModel, где теперь хранится имя схемы (schema_name). Это имя_схемы затем используется для переключения схемы базы данных.

Если cookie аутентификации не существует, промежуточное ПО автоматически устанавливает схему на "public" и проверяет, существует ли на сервере cookie с именем "tenant_id". Если пользователь не прошел аутентификацию и этот cookie существует, промежуточное ПО удаляет его.

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

В настоящее время приложение поддерживает переключаемое имя schema_name для аутентифицированных пользователей, что позволяет мне переключать схемы в зависимости от аутентификации пользователя. Поэтому мне не нужно использовать поддомены или другие префиксы URL.

Я не уверен, что этот подход идеален, так как я относительно новичок в этом деле, но приложение работает, что очень порадовало меня после недели чтения :D Надеюсь, этот совет поможет вам, и спасибо за ваш отзыв.

Middleware

from django.contrib.sessions.backends.db import SessionStore as DBStore из django.db import connection import logging

logger = logging.getLogger(name)

class SessionStore(DBStore): def get_model_class(self): from django.contrib.sessions.models import Session return Session

def save(self, must_create=False):
    with connection.cursor() as cursor:
        logger.debug("Setting search_path to public before saving session")
        cursor.execute("SET search_path TO public")
        super(SessionStore, self).save(must_create)

def load(self):
    with connection.cursor() as cursor:
        logger.debug("Setting search_path to public before loading session")
        cursor.execute("SET search_path TO public")
        return super(SessionStore, self).load()

Shared_session_backend

from django.contrib.sessions.backends.db import SessionStore as DBStore из django.db import connection import logging

logger = logging.getLogger(name)

class SessionStore(DBStore): def get_model_class(self): from django.contrib.sessions.models import Session return Session

def save(self, must_create=False):
    with connection.cursor() as cursor:
        logger.debug("Setting search_path to public before saving session")
        cursor.execute("SET search_path TO public")
        super(SessionStore, self).save(must_create)

def load(self):
    with connection.cursor() as cursor:
        logger.debug("Setting search_path to public before loading session")
        cursor.execute("SET search_path TO public")
        return super(SessionStore, self).load()

Для того чтобы это работало, необходимо добавить поле schema_name в UserModel.

SHARED_SESSION BACKEND
from django.contrib.sessions.backends.db import SessionStore as DBStore из django.db import connection import logging

logger = logging.getLogger(__name__)

class SessionStore(DBStore):
    def get_model_class(self):
        from django.contrib.sessions.models import Session
        return Session

    def save(self, must_create=False):
        with connection.cursor() as cursor:
            logger.debug("Setting search_path to public before saving session")
            cursor.execute("SET search_path TO public")
            super(SessionStore, self).save(must_create)

    def load(self):
        with connection.cursor() as cursor:
            logger.debug("Setting search_path to public before loading session")
            cursor.execute("SET search_path TO public")
            return super(SessionStore, self).load()

SHARED_SESSION_BACKEND

Для того чтобы это работало, необходимо добавить поле schema_name в UserModel.

Вернуться на верх