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

Django REST Framework创建记录前校验关联字段归属用户公司方法

DRF 跨公司越权问题解决方案

一、关联字段归属校验实现

你当前的代码只通过get_queryset过滤了查询范围,没有对写入操作传入的关联ID做归属校验,才会出现越权提交的问题,按以下步骤补全校验即可:

  • 外键字段单字段校验
    对于主单据上的warehouse、contragent这类直接关联的外键,在Serializer中通过validate_<字段名>方法做校验,从请求上下文拿到当前登录用户所属公司,判断传入的关联对象是否属于该公司,不属于则直接抛出校验错误。注意校验时必须带上公司过滤条件查询,不能仅判断对象是否存在,避免传入其他公司的资源ID。
  • 嵌套数据批量校验
    对于items嵌套列表里的product字段,不要在循环里单条查询校验,直接在Serializer的全局validate方法里提取所有商品ID,一次性批量查询当前公司下匹配的商品数量,如果匹配数量和提交的去重后商品ID数量不一致,说明存在不属于当前公司的商品,直接抛出错误即可,性能比循环单查更好。
  • 校验逻辑参考代码
    直接修改你的ConsignmentNoteSerializer即可:
    class ConsignmentNoteSerializer(serializers.ModelSerializer):
        creator = serializers.HiddenField(default=serializers.CurrentUserDefault())
        items = ConsignmentItemSerializer(many=True)
    
        class Meta:
            model = ConsignmentNote
            fields = ['id', 'doc_type', "warehouse", 'date', 'number', 'contragent', 'comment', 'creator', 'items']
            read_only_fields = ['id' ]
    
        def validate_warehouse(self, value):
            user_company = self.context['request'].user.company
            if not Warehouse.objects.filter(id=value.id, company=user_company).exists():
                raise serializers.ValidationError("所选仓库不属于当前公司")
            return value
    
        # 往来单位如果绑定了公司外键,按同样逻辑校验
        def validate_contragent(self, value):
            user_company = self.context['request'].user.company
            if not Contragent.objects.filter(id=value.id, company=user_company).exists():
                raise serializers.ValidationError("所选往来单位不属于当前公司")
            return value
    
        def validate(self, attrs):
            user_company = self.context['request'].user.company
            items = attrs.get('items', [])
            product_ids = [item['product'].id for item in items]
            valid_count = Product.objects.filter(
                id__in=product_ids,
                company=user_company
            ).count()
            if valid_count != len(set(product_ids)):
                raise serializers.ValidationError({"items": "提交商品包含非当前公司所属数据"})
            return attrs
    
        def create(self, validated_data):
            items = validated_data.pop('items')
            note = ConsignmentNote.objects.create(**validated_data)
            for item in items:
                product = item.pop('product')
                ConsignmentItem.objects.create(consignmentnote=note, product=product ,**item)
            return note
    
    这套校验逻辑在创建、更新操作时都会自动执行,能覆盖所有写入场景的越权风险。如果外键字段较多,可以封装通用校验函数减少重复代码。

二、create方法的层级选择

你现在把嵌套单据创建的逻辑写在Serializer层的写法是正确的,两层的职责边界很清晰:

  • Serializer层:负责数据校验、格式转换、模型实例的增改逻辑。尤其是你这种涉及嵌套多表写入的场景,把create逻辑放在Serializer里可以实现逻辑复用——不管哪个View、哪个接口调用这个Serializer,创建单据+关联商品条目的逻辑都能统一生效,不需要重复写代码。
  • View层:负责请求处理、权限校验、查询集过滤、调用Serializer完成数据操作并返回响应。不要把具体的多表创建逻辑堆在View里,否则后期接口增多后代码会极度冗余难以维护。

只有当创建逻辑和请求上下文强耦合(比如需要根据URL路径参数、特殊请求头做和数据本身无关的分支处理)时,才适合在View层重写perform_create做额外处理,核心的对象创建逻辑始终应该收敛在Serializer层。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 21:30:56