在我的 Django 应用程序中,我使用带有隔离数据库的多租户方法。它运行良好,但因为它依赖于每个租户的子域,这是不可扩展的,我正在尝试改变它。为了实现此功能,我尝试使用中间件和会话从用户名中检索租户,并使用它为数据库路由器设置本地变量。逻辑是这样的:
如果用户未登录,BeforeLoginMiddleware 将激活并从用户检索租户名称。因此 username@tenant1 会将 tenant1 设置为会话。这是代码:
import threading
from users.forms import LoginForm
Thread_Local = threading.local()
class BeforeLoginMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path == '/login/':
form = LoginForm(request.POST)
if form.is_valid():
complete_username = form.cleaned_data.get('username')
current_db = complete_username.split('@')[1]
request.session['current_db'] = current_db
request.session.modified = True
response = self.get_response(request)
return response
如果用户已登录,第二个中间件将从会话中检索租户数据,并使用它来定义在数据库路由器上使用的函数上调用的 Thread_Local 变量:
class AppMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
current_db = request.session.get('current_db')
setattr(Thread_Local, 'current_db', current_db)
response = self.get_response(request)
return response
def get_current_db_name():
return getattr(Thread_Local, 'current_db', None)
这是 routers.py 文件:
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
这是我的应用程序上的中间件设置:
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'myapp.middleware.BeforeLoginMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'myapp.middleware.AppMiddleware',
]
这按预期工作,但随机(或者至少看起来是随机)它失去了与数据库的连接,我认为,由于它无法检索会话数据,因此将用户再次重定向到登录页面(我我还使用了 login_required 装饰器)。有时我可以导航到几个不同的页面,一切都很好,然后就断开了连接。有时,登录后导航到主页后的第一页时会断开连接。如果页面空闲大约 1 分钟也会发生这种情况,这是没有意义的,因为我的 SESSION_COOKIE_AGE 设置是 1200。
问题是,我不知道是什么原因造成的,因为没有错误。我注意到发生这种情况时唯一一致的是,它是在浏览器的网络选项卡中注册的 302 状态(这是重定向到登录页面)和消息 Broken pipeline from ('127.0.0.1', 61980) 在终端中,末尾带有此随机代码。
到目前为止我所尝试的,上述行为没有任何改变:
老实说,我已经没有选择了,所以我感谢任何帮助或建议。
回答这个问题以防对其他人有帮助:
我不完全确定这就是问题所在,但我认为依靠会话来检查登录数据是不可靠的。我的意思是,如果与服务器的连接被切断,即使是暂时的,也无法访问数据库,因此用户会被注销。所以我的解决方案是使用 URL 保留租户定义,但我没有不同的子域,而是有不同的 URL 后缀,如下所示:
mysite.com/tenant1
mysite.com/tenant2
这意味着大量的重构。当前的中间件如下所示:
class MyMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
full_path = request.path
tenant = full_path.split('/')[1]
tenants_dict = dict(Tenants.objects.values_list('alias', 'database',))
current_db = tenants_dict.get(tenant)
setattr(threading_file.Thread_Local, 'current_db', current_db)
request.tenant_name = tenant
response = self.get_response(request)
return response
我必须在所有 URL、视图、重定向和模板上添加后缀:
path('<str:tenant_name>/page/', views.page, name='page'),
def page(request, tenant_name):
# view code
redirect('other_page', tenant_name)
<a href="{% url 'page' request.tenant_name %}">Link name</a>
就像我说的,这是一项艰巨的工作,但就我而言,不再处理子域是完全值得的。