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

Django多租户结合graphene-django部署后响应随机异常问题

问题根源

Docker+uWSGI环境下出现串租户/随机返回错误数据,核心原因是uWSGI的worker进程/线程复用机制:django-multitenant用线程本地存储(thread-local)保存租户信息,而生产环境的uWSGI多进程/线程会被重复利用,前一个请求的租户信息没被清理,后续请求复用该进程/线程时,就会拿到错误的租户数据,导致返回其他公司的内容或空结果。

本地开发是单进程单线程,请求结束后线程直接销毁,不会有残留;但生产环境的进程/线程复用机制会把旧的租户信息留下来干扰新请求。

修复方案

1. 完善Django中间件:请求结束强制清理租户

修改SetTenantFromHeadersMiddleware,用try-finally块确保不管请求成功失败,都清空当前租户,避免残留:

class SetTenantFromHeadersMiddleware:
    """
    从请求头设置当前租户,请求结束后强制清理租户信息,避免进程/线程复用导致的串租户问题
    """
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        try:
            if request.path == '/graphql':
                tenant_header = request.META.get('HTTP_N1_TENANT', None)
                if tenant_header:
                    _, company_id = from_global_id(tenant_header)
                    try:
                        company = Company.objects.get(id=company_id)
                        set_current_tenant(company)
                    except Company.DoesNotExist:
                        pass
            response = self.get_response(request)
        finally:
            # 无论请求结果如何,强制清空当前租户
            set_current_tenant(None)
        return response

2. 优化GraphQL中间件:独立初始化租户,避免依赖残留数据

修改MultitenantGraphQLMiddleware,不再依赖线程中可能残留的租户信息,而是从头初始化,同时标记请求已验证租户,避免多字段解析时重复执行逻辑:

from django_multitenant.utils import set_current_tenant, get_current_tenant

class MultitenantGraphQLMiddleware:
    """
    验证租户归属,或为用户设置默认租户,确保每个请求的租户状态正确
    """
    @property
    def safe_mutations(self) -> list[str]:
        return [
            CreateCompany.__name__,
            'TokenAuth',
            'RefreshToken',
        ]

    @property
    def safe_queries(self) -> list[str]:
        return ['Me']

    def is_safe_operation(self, operation) -> bool:
        if not operation:
            return False
        is_mutation = operation.operation == OperationType.MUTATION
        is_safe_mutation = is_mutation and (operation.name.value in self.safe_mutations if operation.name else False)
        is_query = operation.operation == OperationType.QUERY
        is_safe_query = is_query and (operation.name.value in self.safe_queries if operation.name else False)
        return is_safe_mutation or is_safe_query

    def resolve(self, next, root, info, **kwargs):
        if self.is_safe_operation(info.operation):
            set_current_tenant(None)
            return next(root, info, **kwargs)

        # 标记该请求已验证租户,避免GraphQL多字段解析时重复执行
        if hasattr(info.context, '_tenant_verified'):
            return next(root, info, **kwargs)
        
        # 从头初始化租户,不依赖线程残留数据
        tenant_header = info.context.META.get('HTTP_N1_TENANT', None)
        current_tenant = None
        if tenant_header:
            _, company_id = from_global_id(tenant_header)
            try:
                current_tenant = Company.objects.get(id=company_id)
            except Company.DoesNotExist:
                pass
        
        # 验证租户归属或设置默认租户
        if current_tenant:
            if current_tenant.id not in info.context.user.companies.values_list('id', flat=True):
                raise Exception('Provided company ID does not belong to authenticated user')
            set_current_tenant(current_tenant)
        else:
            company = info.context.user.default_company
            if not company:
                raise Exception('Cannot execute GraphQL operation as there is no current tenant')
            set_current_tenant(company)
        
        # 标记已验证
        info.context._tenant_verified = True
        return next(root, info, **kwargs)

3. 调整中间件顺序(关键)

确保中间件执行顺序正确:先验证用户身份,再处理租户逻辑,同时让自定义租户中间件在官方多租户中间件之后执行:

# settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django_multitenant.middlewares.MultitenantMiddleware',
    'api.middleware.SetTenantFromHeadersMiddleware',  # 保持在官方多租户中间件之后
]

GRAPHENE = {
    'SCHEMA': 'api.schema.schema',
    'MIDDLEWARE': [
        'graphql_jwt.middleware.JSONWebTokenMiddleware',  # 先验证JWT,确保用户已登录
        'api.middleware.LoginRequiredMiddleware',  # 再检查登录状态
        'api.middleware.MultitenantGraphQLMiddleware',  # 最后处理租户逻辑
    ]
}

4. 可选:uWSGI配置优化

如果代码修复后仍有问题,可以在uWSGI配置中添加以下参数,减少进程/线程复用的影响(不推荐依赖这个,代码层面修复更可靠):

# uwsgi.ini
enable-threads = true
max-requests = 1000  # 每个worker处理1000个请求后重启,避免残留
reload-on-rss = 2048  # 内存占用超过2G时重启worker
核心原理

django-multitenant用线程本地存储保存当前租户,而uWSGI的worker线程会被复用给多个请求。如果请求结束后不清理租户信息,下一个请求复用该线程时,会继承上一个请求的租户,导致串数据。通过在中间件的finally块强制清空租户,确保每个请求的租户状态都是干净初始化的。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 08:10:24