动态菜单
目标: 开发一个通用的权限组件 以及 一个帮助我们CURD增删改查的stark组件. 将它们应用到CRM业务中!!
# 权限系统介绍
三个问题!
Q1: 为什么需要权限控制?
A1: 程序开发时的权限控制, 对于 不同用户/不同角色的人员 使用系统时候就应该有不同的功能
Q2: 为什么要开发组件?
A2: 假设你今年25岁, 从今天开始写代码到80岁, 每年写5个项目, 那么你的一生就会写275个项目.
保守估计其中应该有150+个都需要用到权限控制, 为了以后不再重复的写代码, 所以就开发一个权限组件以便之后55年的岁月中使用!
Q3: 在web中, 什么是权限?
A3: web程序是通过 url 的切换来查看不同的页面(功能), 所以权限指的其实就是URL, 对url控制就是对权限的控制!!
★ 一个人有多少个权限就取决于他有多少个URL的访问权限!!
# 权限表结构设计
我们深知 权限就是URL!! 那如何设计表结构呢?
# 基于用户
第一版: 基于用户的权限控制 too young too native
# 基于角色
第二版: 基于角色的权限控制 - rbac(Role-Based Access Control).
Q: 若给你一个用户, 需要根据该用户找到它所对应的所有权限.
A: 用户 --> 所拥有的所有角色 --> 再根据角色找到所对应的权限(不同角色所拥有的权限可能会有交集!)
# 客户管理系统
我们用一个简易版的《客户管理》系统为示例, 来实现对其 的权限控制!!
客户系统相关功能的代码在该篇博文中,不会进行呈现,我们需要关注的是 权限控制 是如何实现的!!
武sir提供了该系统的源码, 详看https://www.cnblogs.com/wupeiqi/articles/9178982.html
新建一个python项目dc_crm + 利用pycharm构建虚拟环境
pip install Django==3.2 -i https://pypi.tuna.tsinghua.edu.cn/simple
django-admin startproject dc_crm .
python manage.py startapp rbac apps/rbac # 权限管理
python manage.py startapp web apps/web #《客户系统》
注册app + 进行Django项目快速启动的配置
开始进行客户系统的开发!!
▲《客户管理》系统 大致包含以下功能:
- 客户管理相关: 客户列表、新增客户、修改客户、删除客户、批量导入
- 账单管理相关: 账单列表、新增账单、修改账单、删除账单
1. 开发过程中,我对源码的项目结构进行了重构,采用了 多app + views文件夹 + 多个业务文件夹 !! ★这种结构我超喜欢
2. 武sir提供的 客户系统 相关代码,样式使用的是BootStrap,太丑了!! 我忍不了.(¯﹃¯)
我使用layui对呈现出来的样式进行了更改!!并且添加了 侧边栏收缩展开等前端样式!!
3. 客户系统的每个页面都会继承 layout.html, 这是个前端后台框架代码!!
换个系统,换汤不换药,所以我将其集成到了rbac组件中!!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
回顾一点比较容易混淆的知识吧
■ 模版的查找顺序
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 可以在该路径下找模版文件
'APP_DIRS': True # 可以在注册的app路径下找到templates目录,在该目录下找模版文件
■ 静态文件的查找顺序
STATIC_URL = '/xxx/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'), ]
"""
STATIC_URL必须得有.
若只有STATIC_URL,那么只能在app下创建static文件夹.该文件夹下存放静态文件(img、js、css等)
若写了STATICFILES_DIRS,那么可以在该路径下存放静态文件(img、js、css等)
可以在浏览器中域名进行拼接访问静态文件
http://127.0.0.1:8000/xxx/images/bg.jpg 它会自动去app的static文件夹下找images/bg.jpg,或者去BASE_DIR/static/下找
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
客户管理系统的功能基本开发完成后.
接下来, 我们需要实现 权限控制、动态菜单、面包屑导航、将权限粒度控制到按钮级别等..
路漫漫其修远兮,吾将上下而求索. (´・Д・)」
# 基本的权限控制
快速完成一个基本的权限控制!!
# 准备工作
给基于角色的权限控制的表结构 录入一些数据!!
■ 在rbac组件中,编写权限控制相关的表,并进行数据库迁移!
(¯﹃¯)注: 现在的设计还不是最终版, 但之后的设计都是在此版本基础上扩增的!!
from django.db import models
class Permission(models.Model):
""" 权限表 """
title = models.CharField(verbose_name='标题', max_length=32)
url = models.CharField(verbose_name='含正则的URL', max_length=128)
def __str__(self):
return self.title
class Role(models.Model):
""" 角色表 """
title = models.CharField(verbose_name='角色名称', max_length=32)
permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)
def __str__(self):
return self.title
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)
def __str__(self):
return self.name
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
■ 创建一个超级用户, root
admin123
python manage.py createsuperuser
■ 在rbac组件的admin.py文件中进行admin站点的注册
from django.contrib import admin
from . import models
admin.site.register(models.Permission)
admin.site.register(models.Role)
admin.site.register(models.UserInfo)
2
3
4
5
6
7
■ 以超级管理员的身份登陆项目的 admin后台 , 在 权限组件
中录入相关信息 !!
- 录入权限
- 创建用户
- 创建角色
- 用户分配角色
- 角色分配权限
# - 在此处,展示下,有哪些权限
客户列表, /web/customer/list/
添加客户, /web/customer/add/
删除客户, /web/customer/del/(?P<cid>\d+)/
修改客户, /web/customer/edit/(?P<cid>\d+)/
批量导入, /web/customer/import/
账单列表, /web/payment/list/
添加账单, /web/payment/add/
删除账单, /web/payment/del/(?P<pid>\d+)/
修改账单, /web/payment/edit/(<?P<pid>\d+)/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 关键逻辑
★ 快速完成一个基本权限控制!!
大体的思路:
用户登录时, 根据当前用户找到所有的角色, 再根据角色找到所有的权限, 再将权限信息放入session.
以后每次访问某个url的时候都需要先去session检查是否有权访问!!
- 用户登录, 获取权限信息并放入session
- 用户访问, 在中间件从session中获取用户权限信息
- "YES" session中有权限信息, 对用户当前访问的url进行权限的判断/是否在session的权限信息中!
- "NO" session中没有权限信息, 未登陆或未给当前登陆用户分配权限!!
session默认是在数据库中的,但以后我们可以将session放到memcache、redis等地方!!
★ 为什么去session中校验,而不是每次都去数据库中校验?联表查询性能会降低,减少数据库压力..
2
3
4
5
6
7
(*≧ω≦) 获取当前登陆用户的所有权限信息的ORM查询语句, 值得细品一番!!
★细品点1
current_user是UserInfo表的一条记录/一个对象,roles是该表的多对多外键字段!
正向查询 - 对象.外键字段.all()
可得到外键关联的那张表中的多条记录 <对象.外键字段 跨到了外键关联所在的那张表!!>
current_user.roles.all()
★细品点2
因为 current_user.roles 已经跨到了Role表!! permissions是Role角色表中的多对多外键字段.
So,我们可以通过values来 正向联表查询 指定 查询字段 !!
values [{k1:v1,k2:v2},{k1:v11,k2:v22}]
、values_list [(v1,v2),(v11,v22)]
查询结果都是queryset对象
current_user.roles.all().values("permissions__pk", "permissions__url")
★细品点3
用户:角色 = n:n 角色:权限 = n:n -- 若一个用户拥有多个角色,而这些角色有权限的重合.
So,需要通过distinct()去重处理.
current_user.roles.all().values("permissions__pk", "permissions__url").distinct()
★细品点4
创建了一个角色,并没有给它分配权限,但该角色分配给了用户.
那么在联表查询时候,对该角色的权限查询结果为 {'permissions__pk':None,'permissions__url':None,}
我们应该避免这种情况!!
permissions__isnull=False
保证了 该角色在 角色-权限关联表 中是有记录的/是给该角色分配了权限的
current_user.roles.filter(permissions__isnull=False).values("permissions__pk", "permissions__url").distinct()
# 动态菜单
构造菜单一层 和 菜单两层的数据结构, 然后在模版中循环该数据进行渲染即可!!
一点小知识回顾
- ★★★ 添加 templatetags 模块后, 你需要重启服务器!!! 这样才能在模板中使用 tags 和 filters
- inclusion_tag 找到模版"遵循模版的查找顺序",根据返回值渲染"示例中返回值返回的是字典"
- 若使用的是py3.6及以下的,要注意字典是无序的!! 示例代码使用的是py3.9
2
3
如何实现呢? 简单来说, 权限请求+动态菜单!!
■ 首先 登陆时,做权限和菜单的初始化 - 获取权限信息、获取菜单信息
■ 再次访问 "eg:登陆成功后跳转的客户列表页面!"
中间件进行权限的校验(根据权限信息).
模版中使用inclusion_tag生成动态菜单(根据菜单信息进行动态生成)
# 菜单一层
绝大多数情况下 菜单都不可能只有一层.Hhh
先来看看效果:
xiaoming这个用户只有客户相关的权限,所以动态菜单中没有关于账单的项..
而dengchuan这个用户是有客户和账单相关权限的,所以动态菜单中都有!!
不足之处在于,当我点击 "客户列表"展示的页面里的"添加客户"这些功能时, "客户列表"的选中状态会消失!!
具体思考过程
在前面,我们设计了 基于角色的权限控制 的表结构. 用户-角色-权限 ψ(`∇´)ψ
按照惯性思维,动态菜单的实现,需要再添加一个菜单表,为每个用户分配菜单.
那新增的这个菜单表的字段有哪些呢? 菜单项的名称、点击每个菜单项时跳转的url. So,菜单表的字段至少有id、title、url
你品、你细品,这跟权限表很像啊!! 哇~ 那么我们就不用额外再添加这个菜单表啦!!
★ 我们可以给权限表添加一个字段,字段值为bool类型,表明该条权限记录能否成为菜单! 即 <<权限可能是菜单>>.
举个栗子:
假设当前权限表有四个权限: 客户列表、新增客户、删除客户、修改客户
我们通常会将客户列表作为菜单,点击客户列表,在展示的页面中包含新增、删除、修改客户的功能.
但也可以将新增用户作为菜单中的一项.(这个是可以让管理员自定义的!!)
另外,特别注意,修改和删除客户是不能作为菜单的,why? (\d+)“动态url”对哪个用户进行操作呢?
2
3
4
5
6
7
8
9
10
11
经过上述的分析, 在已有代码的基础上, 我们需要做三件事:
1> 表结构修改, 并手动录入菜单数据. - models.py
2> 获取菜单信息并保存到session中 - init_permission方法中.
3> 模版从session中读取菜单信息,并在页面上显示动态菜单. - inclusion_tag
# 菜单两层
ψ(`∇´)ψ
# 缺陷版
该示例中, 菜单一层和菜单二层的实现效果有一处相同的缺陷
当我点击 "客户列表"展示的页面里的"添加客户"这些非菜单权限功能时, "客户列表"的选中状态会消失!!
先来看看效果.
将菜单一层和菜单两层的关键代码进行比较!!
二层菜单是渲染到下面这样的模版代码中的!!
<div class="layui-side-scroll">
<ul class="layui-nav layui-nav-tree">
{% for level_one in menu_dict.values %}
<li class="layui-nav-item {{ level_one.class }}">
<a href="javascript:">
<i class="layui-icon {{ level_one.icon }}"></i> {{ level_one.title }}
</a>
<dl class="layui-nav-child">
{% for level_two in level_one.children %}
<dd class="{{ level_two.class }}">
<a href="{{ level_two.url }}">{{ level_two.title }}</a>
</dd>
{% endfor %}
</dl>
</li>
{% endfor %}
</ul>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 完美版
我们要解决缺陷版的bug!!
明确需求: 点击非菜单权限时, 一级菜单默认展开,二级菜单默认选中!!
先来看看最终效果!
■ Bug现象:
就菜单二层而言,点击客户编辑按钮时,"客户列表"所属的一级菜单"客户管理"不会展开, 二级菜单"客户列表"没有默认选中的样式.
■ 产生该Bug的原因:
菜单有选中样式的关键判定代码.
菜单一层: current_url = request.path_info
菜单二层: if re.match(f"^{level_two['url']}$", request.path_info)
当我们点击客户编辑按钮时,其url是 /web/customer/edit/2/
, 当前访问的这个url不是 菜单数据结构里的url
■ 解决方案.
给 非菜单的权限/url 做一个 归属/关联!
具体操作:在权限表中实现<自关联>, 将非菜单权限跟菜单权限进行关联.
具体如何实现呢? 在"缺陷版"菜单二层的代码上进行了更改, 更改的地方在截图中都进行了标注!!
精髓在于: 以前是通过url来判定默认选中、现在通过id来判定默认选中!!
# 面包屑导航
路径导航 可以显示 权限之间的层级关系.
先来看看效果!
主要是对登陆获取到的权限信息进行一点处理!
■ 原来:
[
{'id': 1, 'url': '/web/customer/list/', 'pid': None},
{'id': 2, 'url': '/web/customer/add/', 'pid': 1},
{'id': 3, 'url': '/web/customer/del/(?P<cid>\\d+)/', 'pid': 1}
]
■ 现在,变成下方的样子: 权限信息以及其父级权限的信息.
[
{
'id': 1,
'title': '客户列表',
'url': '/web/customer/list/',
'pid': None,
'p_title': None,
'p_url': None
},
{
'id': 2,
'title': '添加客户',
'url': '/web/customer/add/',
'pid': 1,
'p_title': '客户列表',
'p_url': '/web/customer/list/'
},
{
'id': 3,
'title': '删除客户',
'url': '/web/customer/del/(?P<cid>\\d+)/',
'pid': 1,
'p_title': '客户列表',
'p_url': '/web/customer/list/'
}
]
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
在已有代码的基础上, 修改的地方在截图中都进行了标注!!
# 权限粒度-按钮
将权限粒度控制到按钮级别!! ★ 关键在于url的name!!
需求: 若当前登陆用户没有账单的修改和删除权限,那么在账单表格里 修改和删除的按钮 都不应该出现!!
Step1: 给权限表添加一个name字段,先允许为空,在数据库里修改后,再设置为unique=True.
Step2: 权限信息的数据结构还得修改.列表改成字典!!
★ 细数一路以来权限信息的结构经历的变化!!
- 满足基本权限控制
[url,url,url]
- 满足菜单两层完美版
[{"id":..,"url":..,"pid":..},{"id":..,"url":..,"pid":..}]
- 满足面包屑导航
[
{"id":..,"url":..,"pid":..,"title":..,"p_title":..,"p_url":..},
{"id":..,"url":..,"pid":..,"title":..,"p_title":..,"p_url":..}
]
- 满足权限粒度之按钮
{
name:{"id":..,"url":..,"pid":..,"title":..,"p_title":..,"p_url":..}
name:{"id":..,"url":..,"pid":..,"title":..,"p_title":..,"p_url":..}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Step3: 在模版页面添加判断条件 用的filter
看代码截图,其实就改了下 权限信息的数据结构, 其余的都没咋动!!