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.