Django实现DRF接口对象级权限用户系统及问题排查
你选的三个独立ManyToMany字段存储三级对象权限的方案是可行的,完全符合Django开发常规实践,两次迭代遇到的问题都是ORM和DRF的基础用法错误,和方案选型无关。
核心错误根因
- ManyToMany字段不支持default参数:M2M关联依赖独立的中间表,必须先保存主模型实例拿到主键,才能写入关联关系,直接在字段定义上加
default=[]完全不生效。 - DRF嵌套序列化器默认要求写入:如果给M2M字段配置嵌套序列化器但不设置
read_only=True、也不手动实现create/update的关联写入逻辑,DRF会默认把该字段当成必填项,创建时没传对应参数就会报「This field is required」错误。 - 第二次迭代的关联写入逻辑完全失效:你在create方法里写
self.owners = [self.context['creator']]只是给序列化器实例加了个普通属性,根本没有操作数据库层面的CarRecord模型关联,所以创建的记录owners始终为空,CarRecord.objects.filter(owners=user)自然返回空集。 - Admin后台显示异常是配置问题:默认M2M控件本来就会加载所有用户供选择,不是自动关联了全量用户;因为你没正确写入关联数据,才会出现和预期不符的显示效果。
- 初始视图的认证逻辑写反了:
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
相关产品推荐
相关产品推荐

