You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django Tenants子文件夹模式租户切换与登录路由问题求助

Django-Tenants URL前缀切换Schema失效&404问题解决方案

1. 优先排查中间件配置与顺序

django-tenants的中间件顺序直接影响schema切换逻辑,必须保证TenantMiddleware(或自定义租户中间件)在最靠前的位置(仅需放在SecurityMiddleware这类基础中间件之后)。

在settings.py中配置:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    # 自定义租户中间件要放在这里,默认TenantMiddleware同理
    'your_project.middleware.TenantPrefixMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    # ...其他中间件
]

同时确认核心配置项:

# 公共schema的路由配置(处理/login/这类无需租户的路由)
PUBLIC_SCHEMA_URLCONF = 'your_project.urls_public'
# 租户schema的路由配置(必须包含authorization_app的路由)
TENANT_SCHEMA_URLCONF = 'your_project.urls_tenant'

2. 自定义URL前缀中间件的正确实现

自定义前缀识别中间件要确保正确获取租户、切换schema、处理路径重写,以下是可直接复用的示例:

# your_project/middleware.py
from django.http import Http404
from django_tenants.middleware.base import BaseTenantMiddleware
from django_tenants.utils import get_tenant_model

class TenantPrefixMiddleware(BaseTenantMiddleware):
    def get_tenant(self, request):
        # 从URL路径提取前缀,比如/client/test2f0d3775/...
        path_parts = request.path.strip('/').split('/')
        if len(path_parts) < 2 or path_parts[0] != 'client':
            # 非租户路径,返回公共schema
            return get_tenant_model().objects.get(schema_name='public')
        
        tenant_id = path_parts[1]
        try:
            # 根据tenant_id查询租户(假设你的Tenant模型有tenant_id字段)
            return get_tenant_model().objects.get(tenant_id=tenant_id)
        except get_tenant_model().DoesNotExist:
            raise Http404("租户不存在")
    
    def process_request(self, request):
        tenant = self.get_tenant(request)
        # 切换到对应schema
        self.set_tenant(request, tenant)
        # 重写request.path,去掉前缀部分,让租户路由能匹配
        # 比如把/client/test2f0d3775/tenant/login/ 改成 /tenant/login/
        request.path = '/' + '/'.join(request.path.strip('/').split('/')[2:])
        return super().process_request(request)

关键注意点:必须重写request.path去掉前缀,否则租户的路由无法匹配(这是你出现404的核心原因之一)。

3. 路由分层配置

把公共路由和租户路由完全分离,避免冲突:

公共路由(urls_public.py)

只处理不需要租户的请求,比如登录入口:

from django.urls import path
from your_app.views import public_login_view

urlpatterns = [
    path('login/', public_login_view, name='public_login'),
]

租户路由(urls_tenant.py)

包含authorization_app的所有路由,注意不要重复加client/<tenant_id>前缀:

from django.urls import path, include
from authorization_app import urls as auth_urls

urlpatterns = [
    # 这里的路径对应去掉前缀后的部分,比如/tenant/login/
    path('tenant/login/', include(auth_urls)),
    # ...其他租户专属路由
]

4. 修正重定向逻辑

重定向路径/client/tenant_id/tenant/login/没问题,但要确保:

  1. tenant_id是租户模型中存在的有效字段(比如不是schema_name而是自定义的tenant_id)
  2. 重定向时使用reverse生成正确路径,避免硬编码:
# 公共登录视图中重定向的代码示例
from django.shortcuts import redirect, reverse
from django_tenants.utils import get_tenant_model

def public_login_view(request):
    if request.method == 'POST':
        email = request.POST.get('email')
        # 根据邮箱查询对应的租户
        tenant = get_tenant_model().objects.get(user__email=email)
        # 生成租户登录路径
        login_url = reverse('tenant_login')  # 对应租户urls里的name
        # 拼接前缀和登录路径
        redirect_path = f'/client/{tenant.tenant_id}{login_url}'
        return redirect(redirect_path)
    # ...GET请求逻辑

5. 验证Schema切换的正确方式

不要用shell测试请求路径下的schema(shell是独立环境,不会走中间件),直接在中间件或视图中打印验证:

# 在自定义中间件的process_request中添加
print(f"当前Schema: {request.tenant.schema_name}")

# 或者在租户视图中添加
from django.db import connection
print(f"当前Schema: {connection.schema_name}")

启动服务后,访问/client/test2f0d3775/tenant/login/,查看控制台输出,确认schema是否切换成功。

6. 子域名模式Cookie丢失的临时解决

如果后续还要用子域名模式,Cookie丢失是因为domain配置问题,在settings.py中添加:

# 允许所有子域名共享Cookie,注意前面的点
SESSION_COOKIE_DOMAIN = ".127.0.0.1"  # 开发环境,生产环境改成你的主域名
CSRF_COOKIE_DOMAIN = ".127.0.0.1"

内容的提问来源于stack exchange,提问作者user25066324

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 00:43:13