Appearance
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
角色模型类
文件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
部门模型类
文件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
菜单按钮模型类
用途:根据菜单页面,记录了各个按钮对应的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
菜单字段模型类
用途:记录了数据库表名、字段和前端页面菜单的关联关系
文件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
权限表设计
角色菜单权限表
用途:决定角色能否查看某个菜单页面
文件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
角色菜单按钮权限表
用途:决定角色能否查看页面某个菜单按钮
文件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
字段权限表
用途:决定角色能否查看数据表某个字段
文件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
前端页面
部门管理
登录系统,系统管理--部门管理。用于管理用户所属部门,主要实现划分数据权限
菜单管理
登录系统,系统管理--菜单管理。用于实现划分菜单权限,菜单页面包含的按钮操作权限,新增页面(路由)理论都应在此添加菜单所需的一些信息用于权限控制
角色管理
登录系统,系统管理--角色管理。用于角色的增删改查,以及角色的权限配置。
用户管理
登录系统,系统管理--用户管理。用于用户的增删改查、关联用户角色和部门。
新增页面权限添加示例
需求
新增了一个商品信息页面,要求:
- 财务部门用户张三:只能访问订单管理页面并且只能查看
- 物流部门用户李四:增删改查、导出、导入权限
步骤
部门管理
系统配置--部门管理,新增财务部和物流部,部门标识填写相应英文名称
角色管理
系统配置--角色管理,添加角色财务分析师和仓储管理员
角色权限管理
系统配置--角色管理,财务分析师--权限配置:只能访问订单管理页面并且只能查看,最后点击保存菜单权限
仓储管理员--权限配置:增删改查、导出、导入权限,最后点击保存菜单权限
用户管理
系统配置--用户管理,添加用户张三和李四,分别为财务部的财务分析师、物流部的仓储管理员
测试效果
使用张三用户登录,只能访问订单管理页面并且只能查看
使用李四用户登录,有增删改查、导出、导入权限
排错指南
您没有执行该操作的权限
错误描述:提示:【您没有执行该操作的权限。: /api/CrudDemoModelViewSet/】
解决方法:
- 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$
- 登录系统--系统管理--菜单管理,点击商品管理--商品信息菜单,修改按钮权限配置
参考资料
有任何疑问,欢迎在技术交流区留下您的见解,一起交流成长!