Skip to content

RBAC权限管理

RBAC介绍

参考文章:《RBAC基于角色的访问控制 权限管理模型》

后端权限设计

DRF自定义权限

DRF自定义权限关联文件:django-vue3-admin\backend\dvadmin\utils\permission.py,类CustomPermission进行权限检查,重写了方法has_permission(self, request, view)。

在settings.py中,设置项目默认权限策略

python
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated', # 只有经过身份认证确定用户身份才能访问
    ]
}

用户模型类

文件django-vue3-admin\backend\dvadmin\system\models.py,用户表对应用户模型类Users,继承了类AbstractUser和CoreModel。

关注字段:

  • role:关联角色Role,多对多关系
  • post:关联岗位Post,多对多关系,用户所属岗位
  • dept:外键,所属部门Dept一对多关联用户Users,用户所属部门
  • username:唯一标识符unique=True,作为登录时的用户名
python
class Users(CoreModel, AbstractUser):
    username = models.CharField(max_length=150, unique=True, db_index=True, verbose_name="用户账号",
                                help_text="用户账号")
    email = models.EmailField(max_length=255, verbose_name="邮箱", null=True, blank=True, help_text="邮箱")
    mobile = models.CharField(max_length=255, verbose_name="电话", null=True, blank=True, help_text="电话")
    avatar = models.CharField(max_length=255, verbose_name="头像", null=True, blank=True, help_text="头像")
    name = models.CharField(max_length=40, verbose_name="姓名", help_text="姓名")
    GENDER_CHOICES = (
        (0, "未知"),
        (1, "男"),
        (2, "女"),
    )
    gender = models.IntegerField(
        choices=GENDER_CHOICES, default=0, verbose_name="性别", null=True, blank=True, help_text="性别"
    )
    USER_TYPE = (
        (0, "后台用户"),
        (1, "前台用户"),
    )
    user_type = models.IntegerField(
        choices=USER_TYPE, default=0, verbose_name="用户类型", null=True, blank=True, help_text="用户类型"
    )
    post = models.ManyToManyField(to="Post", blank=True, verbose_name="关联岗位", db_constraint=False,
                                  help_text="关联岗位")
    role = models.ManyToManyField(to="Role", blank=True, verbose_name="关联角色", db_constraint=False,
                                  help_text="关联角色")
    dept = models.ForeignKey(
        to="Dept",
        verbose_name="所属部门",
        on_delete=models.PROTECT,
        db_constraint=False,
        null=True,
        blank=True,
        help_text="关联部门",
    )
    login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", help_text="登录错误次数")
    objects = CustomUserManager()

    def set_password(self, raw_password):
        super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest())

    class Meta:
        db_table = table_prefix + "system_users"
        verbose_name = "用户表"
        verbose_name_plural = verbose_name
        ordering = ("-create_datetime",)

在项目的settings.py中指定使用自定义User模型

python
AUTH_USER_MODEL = "system.Users"
USERNAME_FIELD = "username"

用户表默认表名dvadmin_system_users,关联字段设置了不使用外键约束db_constraint=False

image-20241002082736663

角色模型类

文件django-vue3-admin\backend\dvadmin\system\models.py,模型类Role。关注字段:

  • key:权限字符,唯一属性unique=True
python
class Role(CoreModel):
    name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称")
    key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符")
    sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
    status = models.BooleanField(default=True, verbose_name="角色状态", help_text="角色状态")

    class Meta:
        db_table = table_prefix + "system_role"
        verbose_name = "角色表"
        verbose_name_plural = verbose_name
        ordering = ("sort",)

角色表默认表名dvadmin_system_role

image-20241002083502634

部门模型类

文件django-vue3-admin\backend\dvadmin\system\models.py,模型类Dept。关注字段:

  • parent:外键,上级部门Dept一对多关联部门Dept
python
class Dept(CoreModel):
    name = models.CharField(max_length=64, verbose_name="部门名称", help_text="部门名称")
    key = models.CharField(max_length=64, unique=True, null=True, blank=True, verbose_name="关联字符", help_text="关联字符")
    sort = models.IntegerField(default=1, verbose_name="显示排序", help_text="显示排序")
    owner = models.CharField(max_length=32, verbose_name="负责人", null=True, blank=True, help_text="负责人")
    phone = models.CharField(max_length=32, verbose_name="联系电话", null=True, blank=True, help_text="联系电话")
    email = models.EmailField(max_length=32, verbose_name="邮箱", null=True, blank=True, help_text="邮箱")
    status = models.BooleanField(default=True, verbose_name="部门状态", null=True, blank=True, help_text="部门状态")
    parent = models.ForeignKey(
        to="Dept",
        on_delete=models.CASCADE,
        default=None,
        verbose_name="上级部门",
        db_constraint=False,
        null=True,
        blank=True,
        help_text="上级部门",
    )

    @classmethod
    def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None):
        """
        递归获取部门的所有下级部门
        :param dept_id: 需要获取的id
        :param dept_all_list: 所有列表
        :param dept_list: 递归list
        :return:
        """
        if not dept_all_list:
            dept_all_list = Dept.objects.values("id", "parent")
        if dept_list is None:
            dept_list = [dept_id]
        for ele in dept_all_list:
            if ele.get("parent") == dept_id:
                dept_list.append(ele.get("id"))
                cls.recursion_all_dept(ele.get("id"), dept_all_list, dept_list)
        return list(set(dept_list))

    class Meta:
        db_table = table_prefix + "system_dept"
        verbose_name = "部门表"
        verbose_name_plural = verbose_name
        ordering = ("sort",)

岗位模型类

文件django-vue3-admin\backend\dvadmin\system\models.py,模型类Post

python
class Post(CoreModel):
    name = models.CharField(null=False, max_length=64, verbose_name="岗位名称", help_text="岗位名称")
    code = models.CharField(max_length=32, verbose_name="岗位编码", help_text="岗位编码")
    sort = models.IntegerField(default=1, verbose_name="岗位顺序", help_text="岗位顺序")
    STATUS_CHOICES = (
        (0, "离职"),
        (1, "在职"),
    )
    status = models.IntegerField(choices=STATUS_CHOICES, default=1, verbose_name="岗位状态", help_text="岗位状态")

    class Meta:
        db_table = table_prefix + "system_post"
        verbose_name = "岗位表"
        verbose_name_plural = verbose_name
        ordering = ("sort",)

前端权限设计

前端权限划分

  • 页面权限:可看到的页面(菜单
  • 操作权限:可进行的交互行为(页面操作功能按钮
  • 数据权限:可查看的数据(数据行级限制

菜单模型类

用途:记录了菜单页面的名称、路由和组件等信息。

文件django-vue3-admin\backend\dvadmin\system\models.py,模型类Menu。关注字段:

  • parent:外键,上级菜单Menu一对多关联菜单Menu
python
class Menu(CoreModel):
    parent = models.ForeignKey(
        to="Menu",
        on_delete=models.CASCADE,
        verbose_name="上级菜单",
        null=True,
        blank=True,
        db_constraint=False,
        help_text="上级菜单",
    )
    icon = models.CharField(max_length=64, verbose_name="菜单图标", null=True, blank=True, help_text="菜单图标")
    name = models.CharField(max_length=64, verbose_name="菜单名称", help_text="菜单名称")
    sort = models.IntegerField(default=1, verbose_name="显示排序", null=True, blank=True, help_text="显示排序")
    ISLINK_CHOICES = (
        (0, "否"),
        (1, "是"),
    )
    is_link = models.BooleanField(default=False, verbose_name="是否外链", help_text="是否外链")
    link_url = models.CharField(max_length=255, verbose_name="链接地址", null=True, blank=True, help_text="链接地址")
    is_catalog = models.BooleanField(default=False, verbose_name="是否目录", help_text="是否目录")
    web_path = models.CharField(max_length=128, verbose_name="路由地址", null=True, blank=True, help_text="路由地址")
    component = models.CharField(max_length=128, verbose_name="组件地址", null=True, blank=True, help_text="组件地址")
    component_name = models.CharField(max_length=50, verbose_name="组件名称", null=True, blank=True,
                                      help_text="组件名称")
    status = models.BooleanField(default=True, blank=True, verbose_name="菜单状态", help_text="菜单状态")
    cache = models.BooleanField(default=False, blank=True, verbose_name="是否页面缓存", help_text="是否页面缓存")
    visible = models.BooleanField(default=True, blank=True, verbose_name="侧边栏中是否显示",
                                  help_text="侧边栏中是否显示")
    is_iframe = models.BooleanField(default=False, blank=True, verbose_name="框架外显示", help_text="框架外显示")
    is_affix = models.BooleanField(default=False, blank=True, verbose_name="是否固定", help_text="是否固定")

    @classmethod
    def get_all_parent(cls, id: int, all_list=None, nodes=None):
        """
        递归获取给定ID的所有层级
        :param id: 参数ID
        :param all_list: 所有列表
        :param nodes: 递归列表
        :return: nodes
        """
        if not all_list:
            all_list = Menu.objects.values("id", "name", "parent")
        if nodes is None:
            nodes = []
        for ele in all_list:
            if ele.get("id") == id:
                parent_id = ele.get("parent")
                if parent_id is not None:
                    cls.get_all_parent(parent_id, all_list, nodes)
                nodes.append(ele)
        return nodes
    class Meta:
        db_table = table_prefix + "system_menu"
        verbose_name = "菜单表"
        verbose_name_plural = verbose_name
        ordering = ("sort",)

默认表名dvadmin_system_menu

image-20241003081720961

菜单按钮模型类

用途:根据菜单页面,记录了各个按钮对应的API请求地址和方法

文件django-vue3-admin\backend\dvadmin\system\models.py,模型类MenuButton。用于创建菜单页面操作需要的按钮权限的操作方法和名称、路由接口信息等。关注字段:

  • menu:外键,菜单Menu一对多关联菜单按钮MenuButton
python
class MenuButton(CoreModel):
    menu = models.ForeignKey(
        to="Menu",
        db_constraint=False,
        related_name="menuPermission",
        on_delete=models.CASCADE,
        verbose_name="关联菜单",
        help_text="关联菜单",
    )
    name = models.CharField(max_length=64, verbose_name="名称", help_text="名称")
    value = models.CharField(unique=True, max_length=64, verbose_name="权限值", help_text="权限值")
    api = models.CharField(max_length=200, verbose_name="接口地址", help_text="接口地址")
    METHOD_CHOICES = (
        (0, "GET"),
        (1, "POST"),
        (2, "PUT"),
        (3, "DELETE"),
    )
    method = models.IntegerField(default=0, verbose_name="接口请求方法", null=True, blank=True,
                                 help_text="接口请求方法")

    class Meta:
        db_table = table_prefix + "system_menu_button"
        verbose_name = "菜单权限表"
        verbose_name_plural = verbose_name
        ordering = ("-name",)

默认数据库表名:dvadmin_system_menu_button

image-20241003083126764

菜单字段模型类

用途:记录了数据库表名、字段和前端页面菜单的关联关系

文件django-vue3-admin\backend\dvadmin\system\models.py,模型类MenuField。关注字段:

  • menu:外键,菜单Menu一对多关联菜单字段MenuField
python
class MenuField(CoreModel):
    model = models.CharField(max_length=64, verbose_name='表名')
    menu = models.ForeignKey(to='Menu', on_delete=models.CASCADE, verbose_name='菜单', db_constraint=False)
    field_name = models.CharField(max_length=64, verbose_name='模型表字段名')
    title = models.CharField(max_length=64, verbose_name='字段显示名')
    class Meta:
        db_table = table_prefix + "system_menu_field"
        verbose_name = "菜单字段表"
        verbose_name_plural = verbose_name
        ordering = ("id",)

默认表名:dvadmin_system_menu_field

image-20241003092133983

权限表设计

角色菜单权限表

用途:决定角色能否查看某个菜单页面

文件django-vue3-admin\backend\dvadmin\system\models.py,模型类RoleMenuPermission。关注字段:

  • role:外键,角色Role一对多关联角色菜单权限RoleMenuPermission
  • menu:外键,菜单Menu一对多关联角色菜单权限RoleMenuPermission
python
class RoleMenuPermission(CoreModel):
    role = models.ForeignKey(
        to="Role",
        db_constraint=False,
        related_name="role_menu",
        on_delete=models.CASCADE,
        verbose_name="关联角色",
        help_text="关联角色",
    )
    menu = models.ForeignKey(
        to="Menu",
        db_constraint=False,
        related_name="role_menu",
        on_delete=models.CASCADE,
        verbose_name="关联菜单",
        help_text="关联菜单",
    )

    class Meta:
        db_table = table_prefix + "role_menu_permission"
        verbose_name = "角色菜单权限表"
        verbose_name_plural = verbose_name
        # ordering = ("-create_datetime",)

默认表名:dvadmin_role_menu_permission

image-20241003112516011

角色菜单按钮权限表

用途:决定角色能否查看页面某个菜单按钮

文件django-vue3-admin\backend\dvadmin\system\models.py,模型类RoleMenuButtonPermission。关注字段:

  • role:外键,角色Role一对多关联角色按钮权限RoleMenuButtonPermission
  • menu_button:外键,菜单按钮MenuButton一对多关联角色按钮权限RoleMenuButtonPermission
  • dept:数据权限-关联部门,多对多关联部门Dept
py
class RoleMenuButtonPermission(CoreModel):
    role = models.ForeignKey(
        to="Role",
        db_constraint=False,
        related_name="role_menu_button",
        on_delete=models.CASCADE,
        verbose_name="关联角色",
        help_text="关联角色",
    )
    menu_button = models.ForeignKey(
        to="MenuButton",
        db_constraint=False,
        related_name="menu_button_permission",
        on_delete=models.CASCADE,
        verbose_name="关联菜单按钮",
        help_text="关联菜单按钮",
        null=True,
        blank=True
    )
    DATASCOPE_CHOICES = (
        (0, "仅本人数据权限"),
        (1, "本部门及以下数据权限"),
        (2, "本部门数据权限"),
        (3, "全部数据权限"),
        (4, "自定数据权限"),
    )
    data_range = models.IntegerField(default=0, choices=DATASCOPE_CHOICES, verbose_name="数据权限范围",
                                     help_text="数据权限范围")
    dept = models.ManyToManyField(to="Dept", blank=True, verbose_name="数据权限-关联部门", db_constraint=False,
                                  help_text="数据权限-关联部门")

    class Meta:
        db_table = table_prefix + "role_menu_button_permission"
        verbose_name = "角色按钮权限表"
        verbose_name_plural = verbose_name
        ordering = ("-create_datetime",)

默认表名dvadmin_role_menu_button_permission

image-20241003112624676

字段权限表

用途:决定角色能否查看数据表某个字段

文件django-vue3-admin\backend\dvadmin\system\models.py,模型类FieldPermission。关注字段:

  • role:外键,角色Role一对多关联字段权限FieldPermission
  • field:外键,菜单字段MenuField一对多关联字段权限FieldPermission
python
class FieldPermission(CoreModel):
    role = models.ForeignKey(to='Role', on_delete=models.CASCADE, verbose_name='角色', db_constraint=False)
    field = models.ForeignKey(to='MenuField', on_delete=models.CASCADE,related_name='menu_field', verbose_name='字段', db_constraint=False)
    is_query = models.BooleanField(default=1, verbose_name='是否可查询')
    is_create = models.BooleanField(default=1, verbose_name='是否可创建')
    is_update = models.BooleanField(default=1, verbose_name='是否可更新')

    class Meta:
        db_table = table_prefix + "system_field_permission"
        verbose_name = "字段权限表"
        verbose_name_plural = verbose_name
        ordering = ("id",)

默认表名:dvadmin_system_field_permission

image-20241003084121504

前端页面

部门管理

登录系统,系统管理--部门管理。用于管理用户所属部门,主要实现划分数据权限

image-20241003150731454

菜单管理

登录系统,系统管理--菜单管理。用于实现划分菜单权限,菜单页面包含的按钮操作权限,新增页面(路由)理论都应在此添加菜单所需的一些信息用于权限控制

image-20241003151431503

角色管理

登录系统,系统管理--角色管理。用于角色的增删改查,以及角色的权限配置。

image-20241003152430291

用户管理

登录系统,系统管理--用户管理。用于用户的增删改查、关联用户角色和部门。

image-20241003152225558

新增页面权限添加示例

需求

新增了一个商品信息页面,要求:

  • 财务部门用户张三:只能访问订单管理页面并且只能查看
  • 物流部门用户李四:增删改查、导出、导入权限

步骤

部门管理

系统配置--部门管理,新增财务部和物流部,部门标识填写相应英文名称

image-20241004080158974

角色管理

系统配置--角色管理,添加角色财务分析师和仓储管理员

image-20241004080850061image-20241004081013259

角色权限管理

系统配置--角色管理,财务分析师--权限配置:只能访问订单管理页面并且只能查看,最后点击保存菜单权限

image-20241004081408987

image-20241004083110366

仓储管理员--权限配置:增删改查、导出、导入权限,最后点击保存菜单权限

image-20241005154214301

用户管理

系统配置--用户管理,添加用户张三和李四,分别为财务部的财务分析师、物流部的仓储管理员

image-20241004082511852

image-20241004082547741

测试效果

使用张三用户登录,只能访问订单管理页面并且只能查看

image-20241005153915964

使用李四用户登录,有增删改查、导出、导入权限

image-20241005154138608

排错指南

您没有执行该操作的权限

错误描述:提示:【您没有执行该操作的权限。: /api/CrudDemoModelViewSet/】

image-20241004154311048

解决方法:

  • DRF的权限管理,文件django-vue3-admin\backend\dvadmin\utils\permission.py,类CustomPermission进行权限检查。发现变量new_api_ist的值为
sh
['/api/traceability/([a-zA-Z0-9-]+)/:0$', '/api/system/dept_lazy_tree/:0$', '/api/CrudDemoModelViewSet/([a-zA-Z0-9-]+)/:0$', '/api/CrudDemoModelViewSet:0$']

其中/api/CrudDemoModelViewSet:0$,少了一个/,正确值应该为/api/CrudDemoModelViewSet/:0$

  • 登录系统--系统管理--菜单管理,点击商品管理--商品信息菜单,修改按钮权限配置

image-20241005151318789

参考资料


有任何疑问,欢迎在技术交流区留下您的见解,一起交流成长!