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

Django实现DRF接口对象级权限用户系统及问题排查

你选的三个独立ManyToMany字段存储三级对象权限的方案是可行的,完全符合Django开发常规实践,两次迭代遇到的问题都是ORM和DRF的基础用法错误,和方案选型无关。

核心错误根因
  1. ManyToMany字段不支持default参数:M2M关联依赖独立的中间表,必须先保存主模型实例拿到主键,才能写入关联关系,直接在字段定义上加default=[]完全不生效。
  2. DRF嵌套序列化器默认要求写入:如果给M2M字段配置嵌套序列化器但不设置read_only=True、也不手动实现create/update的关联写入逻辑,DRF会默认把该字段当成必填项,创建时没传对应参数就会报「This field is required」错误。
  3. 第二次迭代的关联写入逻辑完全失效:你在create方法里写self.owners = [self.context['creator']]只是给序列化器实例加了个普通属性,根本没有操作数据库层面的CarRecord模型关联,所以创建的记录owners始终为空,CarRecord.objects.filter(owners=user)自然返回空集。
  4. Admin后台显示异常是配置问题:默认M2M控件本来就会加载所有用户供选择,不是自动关联了全量用户;因为你没正确写入关联数据,才会出现和预期不符的显示效果。
  5. 初始视图的认证逻辑写反了:authenticate成功返回用户对象时应该完成登录,返回None才代表账号密码错误需要抛401,不建议自己在接口方法里手写认证逻辑,直接用DRF自带的认证权限组件即可。

可直接落地的实现方案

1. 模型层修正

去掉M2M字段无效的default参数,用auto_now_add自动填充创建时间,给每个关联字段设置独立的related_name避免反向查询冲突:

from django.db import models
from django.contrib.auth.models import User

class CarRecord(models.Model):
    date_created = models.DateTimeField(auto_now_add=True)
    type = models.CharField(max_length=50)
    license = models.CharField(max_length=50)
    creator = models.ForeignKey(
        User,
        on_delete=models.PROTECT, # 不建议用DO_NOTHING,会产生脏数据,保护模式下删除用户前需先转移关联记录
        related_name='created_cars'
    )
    # 三级权限M2M字段
    owners = models.ManyToManyField(User, related_name='owned_cars', verbose_name='所有者')
    editors = models.ManyToManyField(User, related_name='editable_cars', verbose_name='编辑者')
    viewers = models.ManyToManyField(User, related_name='viewable_cars', verbose_name='查看者')

如果后续需要扩展权限有效期、特殊操作权限等能力,只需要把M2M字段改成through中间表模式即可,不需要推翻现有结构。

2. 序列化器修正

读场景返回关联用户的简要信息,写场景不接受前端传入的权限字段(权限由所有者后续单独调整),创建时自动把当前登录用户加入所有者列表:

from rest_framework import serializers
from django.contrib.auth.models import User
from .models import CarRecord

class UserBriefSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username'] # 返回用户id和用户名,方便前端操作

class CarRecordSerializer(serializers.ModelSerializer):
    # 三个权限字段和creator都设为只读,返回时展示关联信息,不接受前端直接写入
    creator = UserBriefSerializer(read_only=True)
    owners = UserBriefSerializer(many=True, read_only=True)
    editors = UserBriefSerializer(many=True, read_only=True)
    viewers = UserBriefSerializer(many=True, read_only=True)

    class Meta:
        model = CarRecord
        fields = '__all__'
        extra_kwargs = {
            'type': {'required': True},
            'license': {'required': True}
        }

    def create(self, validated_data):
        creator = self.context['request'].user
        # 先保存主记录拿到主键
        record = CarRecord.objects.create(
            creator=creator,
            **validated_data
        )
        # 主记录保存后再写入M2M关联,把创建者加入所有者列表
        record.owners.add(creator)
        # editors、viewers默认空,不需要额外操作
        return record

3. 视图层实现

用DRF自带的权限组件做登录校验,GET接口按三类权限分类返回当前用户可访问的记录,补充创建、权限调整、删除的逻辑校验:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from .models import CarRecord
from .serializers import CarRecordSerializer
from django.contrib.auth.models import User

class CarRecordViews(APIView):
    permission_classes = [IsAuthenticated] # 全局要求登录,不需要手写认证逻辑

    def get(self, request):
        user = request.user
        # 分别查询三类权限下的记录,distinct去重避免同一条记录重复返回
        owned_cars = CarRecord.objects.filter(owners=user).distinct()
        editable_cars = CarRecord.objects.filter(editors=user).distinct()
        viewable_cars = CarRecord.objects.filter(viewers=user).distinct()

        return Response({
            'owned': CarRecordSerializer(owned_cars, many=True, context={'request': request}).data,
            'editable': CarRecordSerializer(editable_cars, many=True, context={'request': request}).data,
            'viewable': CarRecordSerializer(viewable_cars, many=True, context={'request': request}).data
        })

    def post(self, request):
        serializer = CarRecordSerializer(data=request.data, context={'request': request})
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data, status=status.HTTP_201_CREATED)

    # 权限调整接口,仅所有者可调用
    def patch(self, request, pk):
        try:
            record = CarRecord.objects.get(pk=pk)
        except CarRecord.DoesNotExist:
            return Response({'error': '记录不存在'}, status=status.HTTP_404_NOT_FOUND)
        
        # 校验当前用户是否为所有者
        if not record.owners.filter(id=request.user.id).exists():
            # 非所有者如果是编辑者,仅允许修改type、license字段,不允许调整权限
            if record.editors.filter(id=request.user.id).exists():
                allowed_fields = {'type', 'license'}
                update_data = {k:v for k,v in request.data.items() if k in allowed_fields}
                CarRecord.objects.filter(pk=pk).update(**update_data)
                record.refresh_from_db()
                return Response(CarRecordSerializer(record, context={'request': request}).data)
            return Response({'error': '无操作权限'}, status=status.HTTP_403_FORBIDDEN)
        
        # 支持传入add_xxx/remove_xxx参数调整三类权限的关联用户
        for action in ['add', 'remove']:
            for role in ['owners', 'editors', 'viewers']:
                param_key = f'{action}_{role}'
                user_ids = request.data.get(param_key, [])
                if not user_ids:
                    continue
                users = User.objects.filter(id__in=user_ids)
                m2m_manager = getattr(record, role)
                if action == 'add':
                    m2m_manager.add(*users)
                else:
                    m2m_manager.remove(*users)
        
        record.save()
        return Response(CarRecordSerializer(record, context={'request': request}).data)

    def delete(self, request, pk):
        try:
            record = CarRecord.objects.get(pk=pk)
        except CarRecord.DoesNotExist:
            return Response({'error': '记录不存在'}, status=status.HTTP_404_NOT_FOUND)
        # 仅所有者可删除记录
        if not record.owners.filter(id=request.user.id).exists():
            return Response({'error': '仅所有者可删除记录'}, status=status.HTTP_403_FORBIDDEN)
        record.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

4. Admin后台优化

注册模型时配置filter_horizontal优化M2M字段的选择体验,避免显示异常:

from django.contrib import admin
from .models import CarRecord

@admin.register(CarRecord)
class CarRecordAdmin(admin.ModelAdmin):
    list_display = ['license', 'type', 'creator', 'date_created']
    filter_horizontal = ['owners', 'editors', 'viewers']

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 12:42:14