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, поэтому, возможно, я упускаю что-то очевидное. Или, может быть, это неправильный способ достичь того, что я хочу. Итак, я что-то упускаю? Как я могу заставить это работать при первой попытке входа?

I have rewritten the entire middleware and changed its logic. I completely stopped using the DomainModel database from django-tenants, which has become redundant in my case.

I started using cookie-based authentication, where the middleware automatically adds the schema name to the server-side cookie for authenticated users.

I also completely stopped using the context_tenant method from django_tenant because it simply didn't respond at all. I replaced it with self.connection.set_schema(schema_name), which works very well in my case.

In the middleware, I fetch the user's cookie ID, decode it, and filter the corresponding record from the UserModel database, where I now store the schema_name. This schema_name is then used to switch the database schema.

If the authentication cookie doesn't exist, the middleware automatically sets the schema to "public" and checks if a cookie named "tenant_id" exists on the server. If the user is not authenticated and this cookie exists, the middleware deletes it.

Since switching the schema loses access to the public schema where the cookie data is stored, I had to rewrite the session backend logic to always look for cookie data in the public schema. This centralizes cookie handling in the "public" schema.

Currently, the application maintains the switched schema_name for authenticated users, which allows me to switch schemas based on user authentication. Therefore, I don't need to use subdomains or other URL prefixes.

I'm not sure if this approach is ideal, as I am relatively new to this, but the application works, which made me quite happy after a week of reading. :D I hope this tip helps you, and thank you for your feedback.

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