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

django-tenants中SimpleJWT跨租户可用的安全问题及方案咨询

Django-Tenants + SimpleJWT 跨租户Token通用问题解决

问题根源

你之前通过中间件动态修改settings.SIMPLE_JWT['SIGNING_KEY']的方案无效,核心原因是:SimpleJWT的核心组件(如Token编码器、认证后端)在应用启动时就已读取配置并缓存了SIGNING_KEY的值,后续修改settings字典无法更新这些组件的内部缓存状态,导致生成/验证Token时仍使用初始密钥。

可行解决方案

要实现租户专属的JWT密钥验证,需要从Token生成和Token验证两个环节入手,直接绑定租户密钥,而非依赖全局settings配置。

1. 完善租户模型,自动生成专属密钥

先确保每个租户都有唯一的JWT密钥,通过信号自动生成避免为空:

from django.db import models
from django_tenants.models import TenantMixin, DomainMixin
from django.db.models.signals import pre_save
from django.dispatch import receiver
import secrets

class Client(TenantMixin):
    name = models.CharField(max_length=100)
    paid_until = models.DateField()
    on_trial = models.BooleanField()
    created_on = models.DateField(auto_now_add=True)
    jwt_secret_key = models.CharField(max_length=100, null=True, blank=True)

    auto_create_schema = True

class Domain(DomainMixin):
    pass

# 自动为新租户生成JWT密钥
@receiver(pre_save, sender=Client)
def generate_jwt_secret(sender, instance, **kwargs):
    if not instance.jwt_secret_key:
        instance.jwt_secret_key = secrets.token_urlsafe(32)

2. 自定义Token生成视图,绑定租户密钥

重写Token获取逻辑,生成Token时使用当前租户的密钥,并将租户schema写入Token payload,方便后续验证:

from rest_framework.response import Response
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.tokens import RefreshToken

class TenantTokenObtainPairView(TokenObtainPairView):
    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        
        # 获取当前租户的密钥
        tenant = request.tenant
        # 使用租户密钥生成Token,同时注入租户schema到payload
        refresh = RefreshToken.for_user(user)
        refresh['schema_name'] = tenant.schema_name
        # 替换Token的签名密钥
        refresh.signing_key = tenant.jwt_secret_key
        
        return Response({
            'refresh': str(refresh),
            'access': str(refresh.access_token),
        })

3. 自定义JWT认证类,按租户验证Token

重写认证逻辑,先从Token payload中提取租户schema,再用对应租户的密钥验证Token有效性:

from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import InvalidToken
from rest_framework_simplejwt.tokens import UntypedToken
from django_tenants.models import TenantMixin

class TenantJWTAuthentication(JWTAuthentication):
    def get_validated_token(self, raw_token):
        try:
            # 先解码Token获取租户schema(不验证签名)
            untyped_token = UntypedToken(raw_token)
            schema_name = untyped_token.payload.get('schema_name')
            
            if not schema_name:
                raise InvalidToken("Token未包含租户标识")
            
            # 获取对应租户
            tenant = TenantMixin.objects.get(schema_name=schema_name)
            # 使用租户专属密钥验证Token
            self.signing_key = tenant.jwt_secret_key
            return super().get_validated_token(raw_token)
        
        except TenantMixin.DoesNotExist:
            raise InvalidToken("无效租户标识")
        except Exception as e:
            raise InvalidToken(str(e))

4. 配置项目使用自定义组件

在settings.py中更新认证类,在urls.py中替换默认Token路由:

# settings.py - REST Framework配置
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'your_app.path.to.TenantJWTAuthentication',
        # 保留其他必要认证类
    ],
}

# urls.py - 替换Token获取路由
from django.urls import path
from your_app.views import TenantTokenObtainPairView

urlpatterns = [
    # ...其他路由
    path('api/token/', TenantTokenObtainPairView.as_view(), name='token_obtain_pair'),
]

关键注意事项

  • 确保request.tenant能正确获取当前租户:django-tenants默认会通过django_tenants.middleware.main.TenantMainMiddleware将租户实例绑定到request对象,需检查该中间件是否在配置列表中。
  • 多线程/异步环境适配:上述方案每次生成/验证Token时都实时获取租户密钥,不存在缓存共享问题,适配多租户部署场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 13:03:11