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
相关产品推荐
相关产品推荐

