Django多租户:如何通过中间件动态更新settings的SITE_HOST_NAME
Django Tenants动态设置SITE_HOST_NAME的正确姿势
首先得提一句:直接在中间件里修改settings.SITE_HOST_NAME是绝对不可取的!Django的settings是全局单例,多请求并发时,一个请求改了这个值会直接影响其他请求,轻则出现域名混乱,重则导致业务bug,这个坑我之前踩过,一定要避开。
回到你的需求:根据当前租户域名动态生成正确的SITEURL,我们换个思路来实现:
1. 重构settings.py的配置逻辑
把原来硬编码的SITE_HOST_NAME和SITEURL改成动态获取的方式,不再依赖全局变量:
# settings.py import os SITE_HOST_SCHEMA = os.getenv('SITE_HOST_SCHEMA', 'http') SITE_HOST_PORT = os.getenv('SITE_HOST_PORT', 8000) # 设置公共租户域名作为 fallback 场景(比如后台任务、无请求时)的默认值 DEFAULT_SITE_HOST_NAME = os.getenv('DEFAULT_SITE_HOST_NAME', 'public.leap.com') def get_current_siteurl(request=None): """动态获取当前请求对应的站点URL""" # 优先从request中取当前租户的域名 if request and hasattr(request, 'tenant_domain'): hostname = request.tenant_domain else: # 无请求场景用默认域名 hostname = DEFAULT_SITE_HOST_NAME # 拼接URL,处理端口逻辑(80/443端口可以省略) if SITE_HOST_PORT and SITE_HOST_PORT not in ('80', '443'): return f"{SITE_HOST_SCHEMA}://{hostname}:{SITE_HOST_PORT}/" else: return f"{SITE_HOST_SCHEMA}://{hostname}/" # 全局SITEURL仅作为默认值,实际业务优先使用上面的动态函数 SITEURL = os.getenv('SITEURL', get_current_siteurl())
2. 修改中间件,把租户域名绑定到request对象
不要碰全局settings,而是把获取到的租户域名存到当前请求的request对象里,这样每个请求都有独立的域名数据:
# my_middleware.py from django.conf import settings from django.db import connection from django.utils.deprecation import MiddlewareMixin as MIDDLEWARE_MIXIN from django_tenants.utils import remove_www, get_tenant_model, get_public_schema_name from customers.models import Domain class CustomTenantMiddleware(MIDDLEWARE_MIXIN): def get_tenant(self, model, hostname, request): return model.objects.get(domain_url=hostname) def hostname_from_request(self, request): """提取并格式化请求的主机名(去掉www和端口)""" return remove_www(request.get_host().split(':')[0]).lower() def process_request(self, request): try: hostname = self.hostname_from_request(request) # 获取当前租户的域名记录 domain_obj = Domain.objects.get(domain=hostname) # 把域名绑定到request对象,供后续业务逻辑调用 request.tenant_domain = domain_obj.domain # 顺便处理Django Tenants的租户切换(确保数据库路由正确指向当前租户) tenant_model = get_tenant_model() tenant = tenant_model.objects.get(domains__domain=hostname) connection.set_tenant(tenant) except Domain.DoesNotExist: # 找不到对应租户时,切换到公共租户 connection.set_schema_to_public() # 用默认域名作为fallback request.tenant_domain = settings.DEFAULT_SITE_HOST_NAME
3. 业务代码中如何使用动态URL
现在不管是视图还是模板,都能轻松拿到当前租户的正确URL:
在视图里使用
from django.conf import settings def my_tenant_view(request): # 获取当前租户的站点URL current_site = settings.get_current_siteurl(request) # 后续业务逻辑直接使用current_site即可 return render(request, 'my_template.html', {'site_url': current_site})
在模板里全局使用
如果想在所有模板中直接调用,可添加一个上下文处理器:
- 在settings的TEMPLATES配置中添加自定义处理器:
# settings.py TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', # 加入自定义上下文处理器 'your_app_name.context_processors.siteurl_processor', ], }, }, ]
- 在你的app下创建
context_processors.py文件:
# your_app_name/context_processors.py from django.conf import settings def siteurl_processor(request): return { 'current_siteurl': settings.get_current_siteurl(request) }
之后在模板里就可以直接用{{ current_siteurl }}获取当前租户的站点URL了。
为什么不能直接修改settings?
再强调一次:Django的settings是全局共享的,启动后所有请求共用同一套配置。如果在中间件里修改settings.SITE_HOST_NAME,多请求并发时会出现值被覆盖的情况——比如A请求刚把值改成t1.leap.com,B请求马上改成t2.leap.com,A请求后续代码拿到的就变成了t2.leap.com,完全混乱。所以必须用每个请求独立的request对象存储租户相关数据,而非修改全局设置。
内容的提问来源于stack exchange,提问作者python enthusiast
相关产品推荐
相关产品推荐

