十九、【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建
【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建
- 前言
- 准备工作
- 第一部分:回顾 Django 内置的 `User` 模型
- 第二部分:设计并创建 `Role` 和 `UserProfile` 模型
- 第三部分:创建 Serializers
- 第四部分:创建 ViewSets
- 第五部分:注册 API 路由
- 第六部分:后端初步测试
- 总结
前言
在上一篇《【用户认证】安全第一步:基于 JWT 的前后端分离认证方案》中,我们成功地为测试平台集成了用户注册和登录功能,并通过 JWT 保护了我们的 API,使用户可以安全地访问平台了。但是,一个成熟的系统通常还需要对用户进行管理,并根据用户的不同职责赋予不同的操作权限。
这篇文章,我们将聚焦于后端基础的搭建:
- 回顾并利用 Django 内置的
User
模型。 - 设计并创建一个自定义的
Role
(角色) 模型。 - 创建
UserProfile
模型,将其与User
模型一对一关联,并在UserProfile
上建立与Role
模型的多对多关联。 - 创建相应的 Serializer 和 ViewSet,提供用户列表和角色管理的基础 API。
为什么需要角色 (Role)?
直接给每个用户单独分配权限会非常繁琐且难以维护。通过引入“角色”的概念,我们可以:
- 将一系列相关的权限打包成一个角色 (例如,“测试工程师”、“项目经理”、“管理员”)。
- 然后将角色分配给用户。
- 当需要调整某类用户的权限时,只需修改对应角色的权限即可,所有拥有该角色的用户权限会自动更新。
这种基于角色的访问控制 (RBAC - Role-Based Access Control) 是一种非常常见且有效的权限管理模型。
准备工作
- Django 后端项目已就绪: 确保你的
test-platform/backend
项目结构完整,用户认证(JWT)已配置。 - 数据库迁移已同步: 之前的模型更改都已
migrate
。 - Postman 或其他 API 测试工具: 用于测试新创建的 API。
- 基础模型 (
BaseModel
) 已定义。
第一部分:回顾 Django 内置的 User
模型
Django 自带了一个强大的用户认证系统,其核心就是 django.contrib.auth.models.User
模型。这个模型已经包含了我们常用的用户字段,如 username
, password
, email
等。我们在上一篇实现用户注册时,就是通过 UserSerializer
和 UserCreateView
与这个模型交互的。
第二部分:设计并创建 Role
和 UserProfile
模型
我们将创建自定义的 Role
模型,并通过一个 UserProfile
模型来扩展 Django 内置的 User
模型,并关联角色。
-
在
api/models.py
中定义Role
和UserProfile
模型:
# test-platform/api/models.py import uuid from django.db import models from django.contrib.auth.models import User # 导入 User 模型# ... (Project, Module, TestCase, TestPlan, TestRun, TestCaseRun 模型保持不变) ...class Role(BaseModel): """角色模型继承自 BaseModel 以获得 name, description, create_time, update_time 字段。"""# permissions 字段将在后续权限篇章中添加# permissions = models.ManyToManyField('Permission', blank=True, verbose_name="角色权限")class Meta:verbose_name = "角色"verbose_name_plural = "角色列表"ordering = ['name'] # 按照名称排序class UserProfile(models.Model):"""用户配置模型,用于扩展内置 User 模型并关联角色"""user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile', verbose_name="关联用户")roles = models.ManyToManyField(Role, blank=True, verbose_name="用户角色", related_name="user_profiles")# 你可以在这里添加其他用户扩展字段,例如:# avatar = models.ImageField(upload_to='avatars/', null=True, blank=True, verbose_name="头像")# phone_number = models.CharField(max_length=20, null=True, blank=True, verbose_name="电话号码")class Meta:verbose_name = "用户配置"verbose_name_plural = "用户配置列表"def __str__(self):return self.user.username# 使用信号机制,在创建 User 时自动创建 UserProfile from django.db.models.signals import post_save from django.dispatch import receiver@receiver(post_save, sender=User) def create_or_update_user_profile(sender, instance, created, **kwargs):if created:UserProfile.objects.create(user=instance)profile, profile_created = UserProfile.objects.get_or_create(user=instance)
代码解释:
Role
模型:继承自BaseModel
,拥有了名称、描述等通用字段。我们暂时不添加permissions
字段,将在后续专门的权限篇中处理。UserProfile
模型:user = models.OneToOneField(User, ...)
: 与 Django 内置的User
模型建立一对一的关联。这意味着每个User
对象最多只能有一个UserProfile
对象,反之亦然。related_name='profile'
允许我们通过user_instance.profile
来访问其关联的UserProfile
。roles = models.ManyToManyField(Role, ...)
: 将UserProfile
与Role
模型建立多对多关系。这样,每个用户 (通过其 Profile) 就可以拥有多个角色。related_name='user_profiles'
允许我们通过role_instance.user_profiles.all()
来获取拥有该角色的所有用户配置。
- 信号 (Signal):
@receiver(post_save, sender=User)
: 这是一个 Django 信号接收器。它会在User
模型实例每次保存 (包括创建和更新) 后被触发。create_or_update_user_profile
函数:if created:
: 如果User
实例是新创建的,则UserProfile.objects.create(user=instance)
会自动为其创建一个关联的UserProfile
对象。UserProfile.objects.get_or_create(user=instance)
: 这是一个更健壮的做法,确保即使用户已存在但由于某种原因没有 Profile,也会为其创建。
-
生成并应用数据库迁移:
在终端中运行:python manage.py makemigrations api python manage.py migrate
第三部分:创建 Serializers
我们需要为 Role
和 User
(及其关联的 UserProfile
) 创建 Serializer,以便在 API 中进行数据转换。
-
创建
RoleSerializer
(api/serializers.py
):# test-platform/api/serializers.py from rest_framework import serializers from .models import Project, Module, TestCase, TestPlan, TestRun, TestCaseRun, Role, UserProfile, User # 确保导入 Role, UserProfile, User from django.contrib.auth.models import Userclass RoleSerializer(serializers.ModelSerializer):class Meta:model = Rolefields = ['id', 'name', 'description', 'create_time', 'update_time'] # 基于 BaseModelread_only_fields = ('create_time', 'update_time')
-
创建/修改
UserSerializer
以处理角色信息 (api/serializers.py
):
我们将创建一个UserDetailSerializer
,用于用户列表的展示和用户信息的更新,其中会包含角色的处理。
# test-platform/api/serializers.py from typing import List # 用于类型提示# UserDetailSerializer (用于用户列表展示和更新,包含角色信息) class UserDetailSerializer(serializers.ModelSerializer):# 通过 UserProfile 获取角色名称列表,用于 GET 请求展示roles = serializers.SerializerMethodField(read_only=True)# 用于 PUT/PATCH 请求时,接收角色 ID 列表来更新用户的角色role_ids = serializers.ListField(child=serializers.IntegerField(), write_only=True, required=False, # 更新用户时不强制要求传递此字段allow_empty=True, # 允许传递空列表以清空用户角色help_text="用户角色ID列表,用于更新用户角色")class Meta:model = Userfields = ('id', 'username', 'email', 'first_name', 'last_name', 'is_active', 'is_staff', 'is_superuser', 'date_joined', 'last_login','roles', # 用于 GET 时展示角色名称'role_ids' # 用于 PUT/PATCH 时提交角色 ID)read_only_fields = ('date_joined', 'last_login') # is_superuser 通常不应通过此 API 修改,由 Django admin 或命令行管理# is_staff 可以根据需求决定是否只读def get_roles(self, obj: User) -> List[str]:"""获取用户关联的角色名称列表。obj 是 User 实例。"""# 确保 UserProfile 存在if hasattr(obj, 'profile') and obj.profile is not None:return [role.name for role in obj.profile.roles.all()]return []def update(self, instance: User, validated_data: dict) -> User:"""自定义更新逻辑,以处理密码和角色的更新。"""# 1. 处理密码更新 (如果提供了新密码)password = validated_data.pop('password', None)if password:instance.set_password(password) # 使用 set_password 来正确哈希# 2. 处理角色更新 (如果提供了 role_ids)role_ids = validated_data.pop('role_ids', None)if role_ids is not None: # 检查 role_ids 是否被传递 (即使是空列表)# 确保 UserProfile 存在profile, created = UserProfile.objects.get_or_create(user=instance)profile.roles.set(role_ids) # 使用 set() 方法直接用 ID 列表更新多对多关系# 3. 更新 User 模型的其他普通字段for attr, value in validated_data.items():setattr(instance, attr, value)instance.save()return instance
UserDetailSerializer
关键点解释:roles = serializers.SerializerMethodField(read_only=True)
: 这个字段用于在API响应中(GET请求)显示用户所拥有的角色名称。它的值由get_roles
方法提供。get_roles(self, obj: User) -> List[str]
: 这个方法接收一个User
实例 (obj
),然后通过obj.profile.roles.all()
获取该用户关联的所有Role
对象,并返回这些角色名称的列表。role_ids = serializers.ListField(...)
: 这个字段用于在API请求中(PUT/PATCH请求)接收一个包含角色ID的列表,以便更新用户的角色分配。write_only=True
: 表示这个字段只在反序列化(输入)时使用,不会包含在序列化(输出)的响应中。required=False
: 表示在更新用户信息时,这个字段不是必须的。allow_empty=True
: 允许传递一个空列表[]
,这通常意味着要移除用户的所有角色。
update(self, instance: User, validated_data: dict) -> User
: 重写了ModelSerializer
的update
方法以实现自定义的更新逻辑:- 密码更新: 如果
validated_data
中包含password
,则使用instance.set_password(password)
来安全地更新密码(进行哈希处理)。 - 角色更新: 如果
validated_data
中包含role_ids
,则获取或创建用户的UserProfile
,然后调用profile.roles.set(role_ids)
。Django ORM 的set()
方法会智能地处理多对多关系的更新:它会移除不再列表中的关联,并添加列表中新增的关联。 - 其他字段更新: 对于其他普通字段,通过遍历
validated_data.items()
并使用setattr()
来更新。
- 密码更新: 如果
第四部分:创建 ViewSets
现在,我们需要为 Role
和 User
创建 ViewSet,以提供 API 端点。
-
创建
RoleViewSet
(api/views.py
):
这个 ViewSet 用于管理角色,通常只有管理员有权限操作。
# test-platform/api/views.py from .models import Role # 导入 Role from .serializers import RoleSerializer # 导入 RoleSerializer # ... (其他导入) ...class RoleViewSet(viewsets.ModelViewSet):"""角色管理 API"""queryset = Role.objects.all().order_by('name')serializer_class = RoleSerializerpermission_classes = [permissions.IsAdminUser] # 仅限管理员访问# 如果角色数量不多,可以不设置分页器,以便前端一次性获取所有角色用于选择# pagination_class = None
权限:
permission_classes = [permissions.IsAdminUser]
确保只有 Django 管理员(通常是is_staff=True
或is_superuser=True
的用户)才能访问这些 API 来管理角色。 -
创建
UserViewSet
(api/views.py
):
这个 ViewSet 用于管理员管理用户列表、查看用户详情和更新用户信息(包括分配角色)。
# test-platform/api/views.py from .serializers import UserDetailSerializer # 确保 UserDetailSerializer 已导入 # ... (其他导入) ...class UserViewSet(viewsets.ModelViewSet):"""用户管理 API (通常仅限管理员访问)"""queryset = User.objects.all().order_by('username').prefetch_related('profile__roles')serializer_class = UserDetailSerializerpermission_classes = [permissions.IsAdminUser] # 仅限管理员访问# 添加过滤、搜索、排序功能filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]filterset_fields = { # 精确匹配'is_active': ['exact'],'is_staff': ['exact'],'profile__roles__id': ['exact'], # 按角色ID筛选用户 (profile__roles__id=role_id)}search_fields = ['username', 'email', 'first_name', 'last_name'] # 模糊搜索ordering_fields = ['username', 'email', 'date_joined', 'last_login']# 通常不通过此ViewSet处理用户创建 (已有 /api/register/)# 如果要禁用创建,可以设置 http_method_names# http_method_names = ['get', 'put', 'patch', 'delete', 'head', 'options']
关键点:
queryset = User.objects.all().order_by('username').prefetch_related('profile__roles')
:prefetch_related('profile__roles')
: 非常重要!这会提前加载每个用户的 Profile 及其关联的所有 Role 对象,避免了在序列化roles
字段时(通过get_roles
方法)对每个用户都进行额外的数据库查询(即 N+1 问题),从而显著提高性能。
serializer_class = UserDetailSerializer
: 使用我们上面定义的,能够处理角色信息的 Serializer。permission_classes = [permissions.IsAdminUser]
: 限制只有管理员可以访问。- 过滤、搜索和排序: 添加了常用的
filter_backends
以支持前端对用户列表进行更灵活的查询。filterset_fields
: 允许通过is_active
,is_staff
以及profile__roles__id
(例如,查找所有拥有某个角色的用户) 进行精确过滤。search_fields
: 允许根据用户名、邮箱等进行模糊搜索。ordering_fields
: 允许前端指定排序字段。
第五部分:注册 API 路由
将新创建的 RoleViewSet
和 UserViewSet
添加到 API 路由中。
-
在
api/urls.py
中注册 ViewSets:
# test-platform/api/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import (ProjectViewSet, ModuleViewSet, TestCaseViewSet, TestPlanViewSet,TestRunViewSet, TestCaseRunViewSet,UserCreateView, RoleViewSet, UserViewSet # 导入新的 ViewSets )router = DefaultRouter()router.register(r'projects', ProjectViewSet, basename='project') router.register(r'modules', ModuleViewSet, basename='module') router.register(r'testcases', TestCaseViewSet, basename='testcase') router.register(r'testplans', TestPlanViewSet, basename='testplan') router.register(r'testruns', TestRunViewSet, basename='testrun') router.register(r'testcaseruns', TestCaseRunViewSet, basename='testcaserun') router.register(r'roles', RoleViewSet, basename='role') # 新增角色路由 router.register(r'users', UserViewSet, basename='user') # 新增用户管理路由urlpatterns = [path('', include(router.urls)),path('register/', UserCreateView.as_view(), name='user_register'),path('', include(router.urls)), # 用户注册接口保持不变 ]
现在,我们有了
/api/roles/
和/api/users/
这两个新的 API 端点。
第六部分:后端初步测试
在进行前端开发之前,强烈建议先通过 API 测试工具(如 Postman、Apifox)来验证后端 API 是否按预期工作。
-
启动 Django 开发服务器。
-
获取管理员 Token:
- 你需要一个管理员账户。如果还没有,通过
python manage.py createsuperuser
创建一个。 - 使用该管理员账户登录 (
POST /api/token/
) 获取 JWT Access Token。
- 你需要一个管理员账户。如果还没有,通过
-
在后续所有请求的
Authorization
头部添加Bearer <admin_access_token>
。 -
测试角色 API (
/api/roles/
):-
创建角色 (POST):
- Body:
{"name": "Tester", "description": "负责执行测试"}
- Body:
{"name": "Manager", "description": "项目经理"}
- 应返回 201 Created。记下创建的角色的 ID。
- Body:
-
获取角色列表 (GET): 应能看到刚创建的角色。
-
获取单个角色详情 (GET
/api/roles/{role_id}/
) -
更新角色 (PUT/PATCH
/api/roles/{role_id}/
)
-
删除角色 (DELETE
/api/roles/{role_id}/
)
-
-
测试用户 API (
/api/users/
):-
获取用户列表 (GET):
- 你应该能看到所有用户(包括创建的超级用户和其他已注册的用户)。
- 检查每个用户的响应数据中是否包含一个名为
roles
的字段,其值是一个角色名称的数组)。
-
获取单个用户详情 (GET
/api/users/{user_id}/
) -
更新用户信息及分配角色 (PUT
/api/users/{user_id}/
):- 选择一个普通用户 (不是超级管理员,除非你确定要修改超管角色) 的 ID。
- Body:
{ "username": "blues","email": "blues_C@example.com", "is_active": true,"role_ids": [1, 2] }
-
尝试移除用户角色: PUT
/api/users/{user_id}/
,Body:{"role_ids": []}
。再次 GET 用户详情,roles
应为空列表。 -
测试用户删除 (DELETE
/api/users/{user_id}/
): 要小心操作。
-
通过这些测试,你可以确保后端为用户和角色管理提供的基础 API 已经准备就绪。
总结
在本文中,我们成功地为测试平台搭建了用户和角色管理的后端基础:
- ✅ 回顾了 Django 内置的
User
模型的重要性。 - ✅ 创建了自定义的
Role
模型,并通过创建UserProfile
模型(与User
一对一关联)的方式,在UserProfile
上建立了与Role
的多对多关系。 同时使用信号机制确保了UserProfile
在用户创建时的自动生成。 - ✅ 定义了
RoleSerializer
用于角色的序列化。 - ✅ 创建了核心的
UserDetailSerializer
,它能够:- 在读取用户数据时,通过
SerializerMethodField
和get_roles
方法高效地(利用prefetch_related
)显示用户的角色名称列表。 - 在更新用户数据时,接收一个
role_ids
列表来方便地设置用户的角色分配,并正确处理密码的更新。
- 在读取用户数据时,通过
- ✅ 创建了
RoleViewSet
(仅限管理员访问) 提供对角色的 CRUD 操作。 - ✅ 创建了
UserViewSet
(仅限管理员访问) 提供对用户的列表、详情获取和更新 (包括角色分配) 操作,并集成了过滤、搜索和排序功能。 - ✅ 注册了相应的 API 路由
/api/roles/
和/api/users/
。 - ✅ 指导了如何使用管理员账户通过 API 测试工具验证这些新的后端 API 端点。
我们的后端已经具备了管理用户、角色以及用户与角色之间关联关系的基本能力。这为下一篇《【用户管理与权限 - 篇二】前端交互:用户管理界面的实现》中构建用户友好的管理界面打下了坚实的数据和接口基础。我们将在下一篇中把这些后端能力通过前端页面呈现出来。