Django阻止用户跨归属创建Product实例的权限问题排查
Django DRF 跨关联模型创建时的权限校验问题修复
问题场景
定义了4层外键关联的模型:
class User(models.Model): name = models.CharField() class Business(models.Model): name = models.CharField() # 商家名称 created_by = models.ForeignKey(User,related_name="businesses",on_delete=models.CASCADE) class ProdCategory(models.Model): business = models.ForeignKey(Business,related_name="categories",on_delete=models.CASCADE) name = models.CharField() class Product(models.Model): category = models.ForeignKey(ProdCategory,related_name="products",on_delete=models.CASCADE) name = models.CharField() # 原代码此处写为ForeignKey为笔误 price = models.DecimalField(max_digits=10, decimal_places=2)
列表查询逻辑正常,可正确返回当前登录用户沿User -> Business -> ProdCategory -> Product链路归属的产品,但创建产品时存在权限漏洞:用户传入其他用户名下的ProdCategory id时,依然可以成功创建产品。
原有视图代码尝试通过重写create方法实现校验,但存在两个问题:
- 传入其他用户的ProdCategory id时校验不生效,依然能创建成功
- 传入当前用户自己的ProdCategory id时抛出错误,提示category字段需为ProdCategory实例,不能传入数值
原有错误视图代码:
class CategoryItemListCreateView(generics.ListCreateAPIView): serializer_class = CategoryItemSerializer def get_queryset(self): return CategoryItem.objects.filter(category__business__created_by=self.request.user).order_by('order') def create(self, request, *args, **kwargs): response = super().create(request, *args, **kwargs) item_data = response.data # 单个商家对应多个分类 categories = ProdCategory.objects.filter(business__created_by=request.user) for c in categories: if item_data['category'] == c.id: Product.objects.create(name=item_data['name'], category=item_data['category']) return response
根因分析
- 校验时机错误:校验逻辑写在
super().create()调用之后,此时父类方法已经完成了数据校验和数据库写入,后续的校验逻辑根本无法拦截非法请求,自然会出现越权创建的问题。 - 外键传参格式错误:调用
Product.objects.create()时直接传入了分类id的数值,Django ORM的外键字段默认要求传入模型实例,若需要直接传id值,需要使用外键名_id的参数格式(比如category_id=item_data['category'])。 - 逻辑冗余低效:遍历当前用户所有分类去比对id的写法完全没有必要,既增加数据库查询压力,逻辑也容易出错。
修复方案
最规范的实现是把权限校验下沉到序列化器层,在数据入库前完成校验,完全移除视图层冗余的手动创建逻辑:
1. 在序列化器中添加分类归属校验
重写序列化器的validate_category方法,在反序列化阶段就校验传入的分类是否属于当前用户:
from rest_framework import serializers from .models import Product, ProdCategory class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = "__all__" def validate_category(self, category_instance): # 传入的category_instance是根据前端传的id查询到的ProdCategory实例 if category_instance.business.created_by != self.context["request"].user: raise serializers.ValidationError("仅可使用自己名下的分类创建产品") return category_instance
DRF通用视图会自动把request对象注入到序列化器的context属性中,无需手动传递。
2. 简化视图层代码
校验逻辑已经在序列化器中实现,不需要重写create方法,直接使用通用视图的默认逻辑即可:
from rest_framework import generics from .serializers import ProductSerializer from .models import Product class ProductListCreateView(generics.ListCreateAPIView): serializer_class = ProductSerializer def get_queryset(self): return Product.objects.filter( category__business__created_by=self.request.user ).order_by("id")
修复后效果:
- 前端传入其他用户名下的分类id时,序列化器校验阶段就会返回400错误,不会写入数据库
- 正常传入自己名下的分类id时,可正常创建产品,不会出现外键类型错误
- 代码逻辑更简洁,没有冗余的数据库查询
内容的提问来源于stack exchange,提问作者Imtiaz Ahmed
相关产品推荐
相关产品推荐

