权限分配
权限分配本应在上一个md文档中阐述的, 奈何上篇文档文字篇幅已经有8千多字了, 所有 权限分配放到了该篇文档中! (´・Д・)」
# 权限分配
说白了, 就是给后台管理员一个页面, 让它可以给用户分配角色, 给角色分配权限!!
需深知: 是不支持直接给用户分配权限的, 需要通过角色!!
# 实现思路
我们分三步走
■ 展示: 在页面上展示用户、角色、权限信息
- 用户、角色的信息很好呈现
- 权限信息包含 一级菜单、二级菜单、以及二级菜单下的子权限, 需要构思适合的数据结构以便在页面上循环出来
■ 筛选: 默认选项! 简单来说,选择用户、角色时,页面上的默认选项 (checkbox是否被选中)
- 点击用户后, 用户所拥有的角色和它所拥有的权限都会默认选中!
- 点击角色后, 应该将该角色所拥有的权限信息都呈现出来!
■ 分配: 点击保存, 进行角色和权限的分配
- 点击用户, 可给当前选中用户分配角色, 点击"保存"按钮进行保存!
- 点击角色, 可给当前选中角色分配权限, 点击"保存"按钮进行保存!
# 第一步: 展现
目标: 在页面上展示 用户、角色、权限信息!!
重点在于理解 如何构建 权限信息的数据结构!! (呈现三级结构)
# 内存地址引用
先来回顾一个python的知识,借此明白两点:
△ 内存地址 △ 列表频繁查找的效率优化.
"""
第一点:对字典的引用.
<[字典是可变类型的数据]> for循环中的item本质是内存地址!!So,字典变了,引用该字典的所有地方都会同步变化!!
第二点:查找优化
频繁的查找id为x的值在列表中是否存在!若每次都是列表查找,都会遍历一遍.
但按照下述步骤将menu_list变成menu_dict去查找,会快很多,因为字典的查找是哈希!!
"""
menu_list = [
{'id': 1, 'title': '客户管理'},
{'id': 2, 'title': '账单管理'},
{'id': 9, 'title': '权限管理'}
]
menu_dict = {}
for item in menu_list:
item['children'] = []
menu_dict[item["id"]] = item
menu_dict[2]['children'] = [11, 22]
"""
[
{'id': 1, 'title': '客户管理', 'children': []},
{'id': 2, 'title': '账单管理', 'children': [11, 22]},
{'id': 9, 'title': '权限管理', 'children': []}]
"""
print(menu_list)
"""
{
1: {'id': 1, 'title': '客户管理', 'children': []},
2: {'id': 2, 'title': '账单管理', 'children': [11, 22]},
9: {'id': 9, 'title': '权限管理', 'children': []}}
"""
print(menu_dict)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 三级数据结构构建
权限信息的数据结构构建
关键代码截图如下:
让我瞅瞅, 权限信息的数据结构长这个样子!!! (*≧ω≦)
[
{
id: 1,
title: "客户管理",
children: []
},
{
id: 2,
title: "账单管理",
children: []
},
{
id: 9,
title: "权限管理",
children: [
{
id: 16,
title: "角色列表",
menu_id: 9,
children: [
{
id: 17,
title: "添加角色",
pid_id: 16
},
{
id: 18,
title: "编辑角色",
pid_id: 16
},
{
id: 19,
title: "删除角色",
pid_id: 16
}
]
},
{
id: 38,
title: "分配列表",
menu_id: 9,
children: [ ]
}
]
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 页面显示
ok, 将数据在页面上 循环展示出来!!
先来看看效果图:
关键代码如下:
# 第二步: 筛选
目标: 默认选项! 简单来说,选择用户、角色时,页面上的默认选项
# 点击用户
完成4个小需求.
先来看看效果:
需求1> 样式改变, 用于区分当前选中的是哪个用户.
需求2> 在第一个面板, 选择用户后, 第二个面板上应该出现 "保存" 按钮!! (用于后续为用户重新分配角色)
需求3> 在第一个面板, 选择用户后, 第二个面板应该自动将 当前选中用户所拥有的所有角色 都默认打上对勾!!
需求4> 在第一个面板, 选择用户后, 第三个面板应该自动将 当前选中用户所拥有的所有权限 都默认打上对勾!!
这四个需求与"三级联动"那进行对比理解:
- 需求1、需求2, 跟前面那"三级联动"的实现逻辑是一样一样的!! ★
- 需求3,需求4 跟"三级联动"那换汤不换药
只不过三级联动那是拿到所有的,在模版中for循环展示;我们这里拿到了,是看选项是否在这个里面,在的话加上选中样式 checked!!
具体来说,根据user_id找到当前用户所分配角色的所有id,在前端循环时判断,若在,就在checkbox中加上checked. 所拥有权限的自动勾选同理.
2
3
4
Ps: 另外,截图中涉及到的多对多的正向查询语句, 在项目一开始实现"基本的权限控制"那就已经编写过啦!!原理一样.
# 点击角色
完成3个小需求. (与点击用户相比, 关键就在于权限面板展示权限的优先级做了点处理!!)
先来看看效果:
需求1> 点击角色, 样式改变, 用于区分当前选中的是哪个角色.
需求2> 在第二个面板, 选择角色后, 第三个面板上应该出现 "保存" 按钮!! (用于后续为角色重新分配权限)
需求3> 在第二个面板, 选择角色后, 第三个面板应该自动将 当前选中角色所拥有的所有角色 都默认打上对勾!!
★ 在权限面板的默认展示中, 选中角色的优先级比选中用户的优先级要高!!
1> 若选中了角色,哪怕还选中了用户,权限面板优先显示选中角色所拥有的权限.
2> 若没有选中角色,但选中了用户,权限面板显示当前选中用户所拥有的所有权限.
关键代码如下, 前端模版中的代码跟前面点击用户实现需求的代码逻辑一样, 不过多赘述..
# 第三步: 分配
为用户分配角色, 为角色分配权限!!
先来看看效果
(¯﹃¯)思考一个问题:
不难想到,我们需要将角色面板和权限面板都用from标签包裹起来.
点击,type="submit"的保存按钮后,该表单中 选中的多选框 都会以 k-v 的形式传递到后端.
<!-- 若不给多选框设置value属性,勾选返回的都是on,设置后返回的是设置的值 -->
<input type="checkbox" name="roles" value="1"> CEO
<input type="checkbox" name="roles" value="2"> 总监
<input type="checkbox" name="roles" value="3"> 员工
后端通过
request.POST.get('表单的name属性值') -- 取最后一个
request.POST.getlist('表单的name属性值') -- 可以接受全部数据 eg:多选框
-- 然后将多对多的关系保存到数据库中!!
Q: 问题来了!! 后端咋知道前端提交的是哪个表单呢??
- 首先我们想到的是 <form method="post" action="?type=x1"> 在action中加上 ?type=x1 进行区分
这个方法我们在 批量处理 页面使用过!! 但是该方法在这里不好使. why? 举个栗子:
当前访问的是 http://127.0.0.1:8000/rbac/distribute/list/?uid=9&rid=2 选中的用户和角色在页面上都有样式
现在我们按照该方法点击保存按钮 会访问 http://127.0.0.1:8000/rbac/distribute/list/?type=x1 页面上的选中效果消失了!!
所以,该方法不适合该场景
- 正解: 使用隐藏的input表单来实现!!
★ <input type="hidden" name="type" value="role">
<input type="hidden" name="type" value="permission">
后端
if request.method == "POST" and request.POST.get("type") == "role":
pass # 点击的是角色面板的保存
if request.method == "POST" and request.POST.get("type") == "permission":
pass # 点击的是权限面板的保存
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
关键代码如下:
一点小细节
{% block js %}
<script>
window.onload = function () {
// 用于保存成功后,告知用户!!
let value = "{{ msg }}";
if (value) {
layer.msg(value);
}
};
$(function () {
// ■ 首先要明白一点,右键检查html代码,在页面上给复选框打勾后,对应的checkbox的input元素看起来是没有添加checked的!!
// 但是你获取该勾选的多选框,该元素.prop("checked") 值为True
// ■ 再记录下使用layui的checkbox的input 选择框踩的坑.
// - 你右键检查,在html代码中选中input元素,页面上不会对应高亮的.
// 而检查页面上对应的选择框,html代码中高亮的是input的相邻的下一个元素
// So.点击事件应作用于该元素!!
$('.dc-check-all .layui-unselect').on('click', function () {
var obj = $(this).prev("input")
$(this).parents('.layui-colla-title').next().find(':checkbox').prop('checked', obj.prop('checked'));
if (obj.prop('checked')) {
$(this).parents('.layui-colla-title').next().find('.layui-unselect').addClass("layui-form-checked")
} else {
$(this).parents('.layui-colla-title').next().find('.layui-unselect').removeClass("layui-form-checked")
}
});
})
</script>
{% endblock %}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# RBAC组件应用及完善
在前面的篇幅中, 我们已经开发完成了 RBAC组件 的功能!! 接下来, 需要对其进行应用!!
# 准备工作
我们重新开发一个主机管理程序 来逐步应用 现目前的RBAC组件!!
新建一个python项目host_manage + 利用pycharm构建虚拟环境
pip install Django==3.2 -i https://pypi.tuna.tsinghua.edu.cn/simple
django-admin startproject host_manage .
python manage.py startapp app01
- 项目根目录下创建apps文件,将app01放到里面
- 记得将 apps/app01/apps.py 里的 name = 'app01' 改为 name = 'apps.app01'
- 注册app 'apps.app01.apps.App01Config',
将我们现目前写好的RBAC组件也放到apps目录下,并注册app!!
- ★ 记得将RBAC组件下的 迁移文件 删除!!
进行Django项目快速启动的配置
2
3
4
5
6
7
8
9
10
接下来, 开始 主机管理程序的业务开发. 首当其冲的是, 给主机管理程序进行数据库表的设计!!
# 用户表方案
思考: 业务里的用户表需与rbac组件里的用户表创建关系, 如何创建?
共提供了两个方案, 各有利弊, 最终, 我们采用方案二!!
# 方案一
业务的用户表里 使用 O2O 一对一的外键!!
user = models.OneToOneField(verbose_name="用户", to=RbacUserInfo, on_delete=models.CASCADE)
!!
优缺点:
■ 缺点
用户信息散落在两处,在app01和rbac里都有一部分用户信息/各自存了几列用户数据.
用户管理功能肯定得将这两处整合到一起进行管理.
就目前现状而言,经过O2O的设计后:
rbac里原来的用户管理代码肯定满足不了(eg:字段相较以前,变多了,增加了几个..)
So,需要在app01里重新写一个! 比如,业务的用户表对象取名字 obj.user.name
■ 优点
在app01里重新写的代码逻辑,很多都可以借鉴rbac里原来用户管理里的代码,都倒差不差!!
2
3
4
5
6
7
8
# 方案二(推荐)
不要分散在两处, 就都放到 业务的用户表中!!
★ 使用继承! + abstract=True
优缺点:
■ 优点
将用户的所有信息都放到了一张表中(业务的用户表中),对用户信息的维护会更加便捷!!
■ 缺点
在rbac中所有关于用户的操作,都不能使用啦!!
rbac中有两处用了rbac的用户表:
- 用户管理里的CURD+密码重置 => 在rbac中将相关的路由配置注释点
- 权限分配的第一个面板中会展示所有用户信息,当时读取的就是rbac里的用户表 => 做调整,让它去读区app01业务里的用户表
因为使用了abstract=True,所以在rbac组件里admin.py里对rbac的用户表的admin注册将会报错,将其注释掉!!
2
3
4
5
6
7
8
9
10
# 开始修改
我们已经确定了使用方案二 -
继承! + abstract=True
我们需要对现已有rbac组件代码进行以下修改:
1. 在rbac组件中有关rbac用户表的CURD+密码重置的相关路由配置进行注释.
▲ 意味着用户管理需要在 业务的用户表里 进行操作!!
2. rbac组件的权限分配也用到了rbac的用户表! 权限分配应该用业务中的用户表!
So,在权限分配之时,读取用户表变成通过配置文件来进行指定并导入.
2
3
4
# 业务逻辑开发
开始进行自己的业务开发
注: 在进行业务开发时, 先注释掉母板里的 动态菜单和面包屑导航!! (因为业务开发的模版会继承rbac里的母板
- 部门表 单表的CURD
- 用户表的CURD+重置密码
- 其实大体上跟原来rbac里用户管理的代码差不多,相比下就是多了些字段 {{ row.get_level_display }} {{ row.depart.title }}..
- 考虑到有很多用户的话,需要使用分页,分页需要携带原搜索条件,所以会使用 memory_url、memory_reverse 望周知!!
- 主机表的CURD
具体的代码就不做过多阐述啦!!都是学过用过的,经常用的.(¯﹃¯) 我咔咔咔一通cv大法!!
2
3
4
5
6
7
就这一通简单的开发下来, 很烦躁, 很多代码都是相似的, 所以 后面我们会开发stark组件来解决这个问题!!
# 注销和首页
添加了 注销和首页的路由!!
Q: 着重思考一个问题: 对于首页、登陆、注销的路由是否需要分配权限呢?
A: 登陆在白名单里,直接就可访问 ; 注销和首页是登陆后才能访问的路由,但无需经过权限验证!!
这导致了, 在中间件里 会分为三种情况: 白名单、登陆但无需权限检验、登陆且需权限校验!! ★
因为我倾向于把登陆功能放到了rbac组件里, 所以 登陆逻辑里使用的 用户表 也要通过 配置文件来进行指定并导入!!
# 应用rbac
业务开发完了, rbac组件也完善的差不多了.
现在开始使用rbac组件来实现对当前开发的主机管理系统的 权限验证、权限批量添加、权限分配等!!
进行下述操作.
当前是注释掉母板里的 动态菜单和面包屑导航 以及 中间件配置 的代码的. 依次进行下面的配置:
- http://127.0.0.1:8000/rbac/menu/list/ 在该页面添加1级菜单, 用户管理、权限管理、主机管理
- http://127.0.0.1:8000/rbac/menu/mutil/permissions/ 在该页面批量添加权限,并批量更新它们的所属关系!!
提醒一点,给权限添加了所属的一级菜单后,该权限就默认变成了可做二级菜单的权限啦!!
- http://127.0.0.1:8000/web/user/list/ 添加一个用户"武沛齐"
- http://127.0.0.1:8000/rbac/role/list/ 添加一个角色"root"
- http://127.0.0.1:8000/rbac/distribute/list/ 在该页面分配权限,给一个用户分配root角色,root角色分配所有权限
■ 至此,你就拥有了一个超级用户"武沛齐"
- 将layui.html中 动态菜单和面包屑导航的代码应用上
- 中间件注册上 'apps.rbac.utils.verify.RbacMiddleware',
- 白名单的处理 VALID_URL_LIST
- 需要登陆但无需权限校验的路由 NO_PERMISSION_LIST
- 权限的初始化 PERMISSION_SESSION_KEY、MENU_SESSION_KEY
- 批量操作权限时,自动化发现路由中URL时,排除的URL AUTO_DISCOVER_EXCLUDE
做完上述操作,rbac成功配置完毕!! 撒花Bingo~
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# RBAC组件使用文档
来总结下, 一个新项目如何快速的应用上 rbac组件!!
Step1: 将rbac组件拷贝项目, 并进行app注册 (使用多app的配置, 放到项目根目录的apps文件夹下!!
Step2: 将rbac组件中 的数据库迁移记录 全部删除.
Step3: 进行业务系统中用户表结构的设计. 遵循: 业务表结构中的用户表需要和rbac中的用户有继承关系
# -- rbac组件的用户表
class UserInfo(models.Model):
"""用户表"""
name = models.CharField(verbose_name='用户名', max_length=32)
password = models.CharField(verbose_name='密码', max_length=64)
email = models.CharField(verbose_name='邮箱', max_length=32)
roles = models.ManyToManyField(verbose_name='拥有的所有角色', to=Role, blank=True)
class Meta:
abstract = True
# -- 业务系统中的用户表
from apps.rbac.models import UserInfo as RbacUserInfo
class UserInfo(RbacUserInfo):
"""用户表"""
phone = models.CharField(verbose_name='联系方式', max_length=32)
level_choices = (
(1, 'T1'),
(2, 'T2'),
(3, 'T3'),
)
level = models.IntegerField(verbose_name='级别', choices=level_choices)
depart = models.ForeignKey(verbose_name='部门', to='Department', on_delete=models.CASCADE)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
同时, 将业务系统中的用户表的路径写到配置文件!! 用于在rbac分配权限时, 读取业务表中的用户信息!!
# -- settings.py
RBAC_USER_MODLE_CLASS = "apps.app01.models.UserInfo" # 业务系统中的用户表
2
Step4: 开始业务逻辑的开发!! 注意几点.
1> 你可以使用 rabc里的 layout.html 模版! 但切记, 开发业务系统时需将 layout.html 里的 动态菜单和面包屑 的代码注释掉!!
2> 业务系统开发的所有路由, 都必须设置name!! 因为需要用它反向生成url + 自动发现路由 + 按钮级别粒度的控制.
# -- apps/rbac/templates/rbac/layout.html
"""
<div class="layui-side layui-bg-black sidebar dc_sidebar" id="dc_sidebar">
<!-- 侧边栏区域 动态菜单-自动生成 ■“业务系统开发过程中注释掉” -->
{% multi_menu request %}
</div>
<div class="content dc_content" id="dc_content">
<!-- 内容主体区域 -->
<div style="padding: 35px; position: relative;">
<div style="position: absolute;left: 3px;top:3px" onclick="toggleSidebar()">
<i class="layui-icon layui-icon-release" style="font-size: 20px; color: slategray;"></i>
</div>
<div style="margin-bottom: 25px">
<!-- 面包屑导航 自动生成 ■“业务系统开发过程中注释掉”-->
{% url_record request %}
</div>
{% block content %} {% endblock %}
</div>
</div>
"""
# -- 业务系统的路由
url(r'^host/list/$', host.host_list, name='host_list'),
url(r'^host/add/$', host.host_add, name='host_add'),
url(r'^host/edit/(?P<pk>\d+)/$', host.host_edit, name='host_edit'),
url(r'^host/del/(?P<pk>\d+)/$', host.host_del, name='host_del'),
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Step5: 进行权限信息的录入
首先, 你需要在根路由中 添加 rbac组件的路由分发, 注意哦, 必须设置其namespace为"rbac"
path('rbac/', include('apps.rbac.urls', namespace="rbac")), # rbac组件
path('web/', include('apps.app01.urls')), # 业务系统
2
接着, 你要知道在批量添加页面是会自动发现项目中的路由的, 有些路由是无需自动发现的!!
# -- settings.py
# 批量操作权限时,自动化发现路由中URL时,排除的URL
AUTO_DISCOVER_EXCLUDE = [
'/admin/.*',
'/rbac/login/',
'/rbac/logout/',
'/rbac/index/',
]
# -- 上面登陆、登出、首页使用的是rbac组件内部的,但大多数时候我们会在业务app里自己写并且将其路由配置到根路由里
# 所以,该场景下,排除的URL配置,应该像下面这样写 (- 将rbac里登陆、登出、首页的路由配置注释掉就行!!)
AUTO_DISCOVER_EXCLUDE = [
'/admin/.*',
'/login/',
'/logout/',
'/index/',
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
接着, 依次在下面地址中进行操作
- http://127.0.0.1:8000/rbac/menu/list/ 在该页面添加一级菜单, eg: 用户管理、权限管理、主机管理
- http://127.0.0.1:8000/rbac/menu/mutil/permissions/ 在该页面批量添加权限,并批量更新它们的所属关系!!
提醒一点,给权限添加了所属的一级菜单后,该权限就默认变成了可做二级菜单的权限啦!!
- http://127.0.0.1:8000/web/user/list/ eg: 添加一个用户"武沛齐"
- http://127.0.0.1:8000/rbac/role/list/ eg: 添加一个角色"root"
- http://127.0.0.1:8000/rbac/distribute/list/ 在该页面分配权限,给"武沛齐"用户分配root角色,root角色分配所有权限
■ 至此,你就拥有了一个超级用户"武沛齐"
2
3
4
5
6
7
Step6: 编写用户的登录逻辑
你可以使用rbac自带的登录、注销、首页 的逻辑!! 当然, 你偏要自己写的话, 有一点你必须注意:
# -- must do it (one): 在用户登录成功后,执行该语句!!
from apps.rbac.utils.initialize import init_permission
init_permission(current_user, request) # 用户权限信息的初始化
# -- must do it (two): settings需进行相关配置
# 权限在Session中存储的key
PERMISSION_SESSION_KEY = "permission_url_list_key"
# 菜单在Session中存储的key
MENU_SESSION_KEY = "menu_list_key"
2
3
4
5
6
7
8
9
Step7: 在setting中注册rbac中用于权限校验的中间件!! 以及相关的配置.
MIDDLEWARE = [
# ... ... ...
'apps.rbac.utils.verify.RbacMiddleware',
]
# 白名单,无需登陆即可访问
VALID_URL_LIST = [
'/rbac/login/',
'/admin/.*',
'/favicon.ico',
]
# 需要登录但无需权限的URL / 登陆后的每个人都默认拥有的权限 / 登陆后不用做权限校验的路由
NO_PERMISSION_LIST = [
'/rbac/index/',
'/rbac/logout/',
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Ok, 到这一步, 你就可以 将layout.html 里 动态菜单和面包屑 的相关代码 的注释取消掉/应用上啦!!
Step8: 在业务系统的前端模版中, 进行粒度到按钮级别的控制
{% load button_permission %}
{% if request|has_permission:'host_edit' %}
<a href="{% memory_url request 'host_edit' row.pk %}">
<button type="button" class="layui-btn layui-btn-sm layui-btn-primary">
<i class="layui-icon layui-icon-edit"></i>
</button>
</a>
{% endif %}
2
3
4
5
6
7
8
9
说在后面, 简略版:
总结,目的是希望在任意系统中应用权限系统。
- 用户登录 + 用户首页 + 用户注销 业务逻辑
- 项目业务逻辑开发
注意: 开发时候灵活的去设置layout.html中的两个inclusion_tag "开发时,去掉;上下线运行时,取回"
- 权限信息的录入
- 配置文件
# 注册APP
INSTALLED_APPS = [
# ... ... ...
'apps.rbac.apps.RbacConfig',
]
# 应用中间件
MIDDLEWARE = [
# ... ... ...
'apps.rbac.utils.verify.RbacMiddleware',
]
# #################### 权限相关配置 #######################
# 业务中的用户表
RBAC_USER_MODLE_CLASS = "apps.app01.models.UserInfo"
# 权限在Session中存储的key
PERMISSION_SESSION_KEY = "permission_url_list_key"
# 菜单在Session中存储的key
MENU_SESSION_KEY = "menu_list_key"
# 白名单,无需登陆即可访问
VALID_URL_LIST = [
'/rbac/login/',
'/admin/.*',
'/favicon.ico',
]
# 需要登录但无需权限的URL / 登陆后的每个人都默认拥有的权限 / 登陆后不用做权限校验的路由
NO_PERMISSION_LIST = [
'/rbac/index/',
'/rbac/logout/',
]
# 批量操作权限时,自动化发现路由中URL时,排除的URL
AUTO_DISCOVER_EXCLUDE = [
'/admin/.*',
'/rbac/login/',
'/rbac/logout/',
'/rbac/index/',
]
- 粒度到按钮级别的控制
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
大工告成!!