DC's blog DC's blog
首页
  • 计算机基础
  • linux基础
  • mysql
  • git
  • 数据结构与算法
  • axure
  • english
  • docker
  • opp
  • oop
  • 网络并发编程
  • 不基础的py基础
  • 设计模式
  • html
  • css
  • javascript
  • jquery
  • UI
  • 第一次学vue
  • 第二次学vue
  • Django
  • drf
  • drf_re
  • 温故知新
  • flask
  • 前后端不分离

    • BBS
    • 订单系统
    • CRM
  • 前后端部分分离

    • pear-admin-flask
    • pear-admin-django
  • 前后端分离

    • 供应链系统
  • 理论基础
  • py数据分析包
  • 机器学习
  • 深度学习
  • 华中科大的网课
  • cursor
  • deepseek
  • 杂文
  • 罗老师语录
  • 关于我

    • me
  • 分类
  • 归档
GitHub (opens new window)

DC

愿我一生欢喜,不为世俗所及.
首页
  • 计算机基础
  • linux基础
  • mysql
  • git
  • 数据结构与算法
  • axure
  • english
  • docker
  • opp
  • oop
  • 网络并发编程
  • 不基础的py基础
  • 设计模式
  • html
  • css
  • javascript
  • jquery
  • UI
  • 第一次学vue
  • 第二次学vue
  • Django
  • drf
  • drf_re
  • 温故知新
  • flask
  • 前后端不分离

    • BBS
    • 订单系统
    • CRM
  • 前后端部分分离

    • pear-admin-flask
    • pear-admin-django
  • 前后端分离

    • 供应链系统
  • 理论基础
  • py数据分析包
  • 机器学习
  • 深度学习
  • 华中科大的网课
  • cursor
  • deepseek
  • 杂文
  • 罗老师语录
  • 关于我

    • me
  • 分类
  • 归档
GitHub (opens new window)
  • BBS

  • 订单平台

  • CRM

    • rbac

      • 动态菜单
        • 权限系统介绍
        • 权限表结构设计
          • 基于用户
          • 基于角色
        • 客户管理系统
        • 基本的权限控制
          • 准备工作
          • 关键逻辑
        • 动态菜单
          • 菜单一层
          • 菜单两层
          • 缺陷版
          • 完美版
        • 面包屑导航
        • 权限粒度-按钮
      • 菜单和权限管理
      • 权限分配
    • stark

    • crm

  • flask+layui

  • django+layui

  • 供应链

  • 实战
  • CRM
  • rbac
DC
2024-02-28
目录

动态菜单

目标: 开发一个通用的权限组件 以及 一个帮助我们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

image-20240228190554126

# 基于角色

第二版: 基于角色的权限控制 - rbac(Role-Based Access Control).

Q: 若给你一个用户, 需要根据该用户找到它所对应的所有权限.
A: 用户 --> 所拥有的所有角色 --> 再根据角色找到所对应的权限(不同角色所拥有的权限可能会有交集!)

image-20240228192455815


# 客户管理系统

我们用一个简易版的《客户管理》系统为示例, 来实现对其 的权限控制!!

客户系统相关功能的代码在该篇博文中,不会进行呈现,我们需要关注的是 权限控制 是如何实现的!!
武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组件中!!
1
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/下找
"""
1
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
1
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
1

■ 在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)
1
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+)/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 关键逻辑

★ 快速完成一个基本权限控制!!

per

大体的思路:
用户登录时, 根据当前用户找到所有的角色, 再根据角色找到所有的权限, 再将权限信息放入session.
以后每次访问某个url的时候都需要先去session检查是否有权访问!!

- 用户登录, 获取权限信息并放入session
- 用户访问, 在中间件从session中获取用户权限信息
  - "YES" session中有权限信息, 对用户当前访问的url进行权限的判断/是否在session的权限信息中!
  - "NO"  session中没有权限信息, 未登陆或未给当前登陆用户分配权限!!

session默认是在数据库中的,但以后我们可以将session放到memcache、redis等地方!!
★ 为什么去session中校验,而不是每次都去数据库中校验?联表查询性能会降低,减少数据库压力..
1
2
3
4
5
6
7

image-20240301131054692

(*≧ω≦) 获取当前登陆用户的所有权限信息的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
1
2
3

如何实现呢? 简单来说, 权限请求+动态菜单!!
■ 首先 登陆时,做权限和菜单的初始化 - 获取权限信息、获取菜单信息
■ 再次访问 "eg:登陆成功后跳转的客户列表页面!"
     中间件进行权限的校验(根据权限信息).
     模版中使用inclusion_tag生成动态菜单(根据菜单信息进行动态生成)

# 菜单一层

绝大多数情况下 菜单都不可能只有一层.Hhh

先来看看效果:
xiaoming这个用户只有客户相关的权限,所以动态菜单中没有关于账单的项..
而dengchuan这个用户是有客户和账单相关权限的,所以动态菜单中都有!!
不足之处在于,当我点击 "客户列表"展示的页面里的"添加客户"这些功能时, "客户列表"的选中状态会消失!!

33

具体思考过程

在前面,我们设计了 基于角色的权限控制 的表结构. 用户-角色-权限 ψ(`∇´)ψ
按照惯性思维,动态菜单的实现,需要再添加一个菜单表,为每个用户分配菜单.
那新增的这个菜单表的字段有哪些呢? 菜单项的名称、点击每个菜单项时跳转的url. So,菜单表的字段至少有id、title、url 

你品、你细品,这跟权限表很像啊!! 哇~ 那么我们就不用额外再添加这个菜单表啦!!
★ 我们可以给权限表添加一个字段,字段值为bool类型,表明该条权限记录能否成为菜单! 即 <<权限可能是菜单>>.
举个栗子:
  假设当前权限表有四个权限: 客户列表、新增客户、删除客户、修改客户
  我们通常会将客户列表作为菜单,点击客户列表,在展示的页面中包含新增、删除、修改客户的功能.
  但也可以将新增用户作为菜单中的一项.(这个是可以让管理员自定义的!!)
  另外,特别注意,修改和删除客户是不能作为菜单的,why? (\d+)“动态url”对哪个用户进行操作呢?
1
2
3
4
5
6
7
8
9
10
11

经过上述的分析, 在已有代码的基础上, 我们需要做三件事:
1> 表结构修改, 并手动录入菜单数据. - models.py
2> 获取菜单信息并保存到session中 - init_permission方法中.
3> 模版从session中读取菜单信息,并在页面上显示动态菜单. - inclusion_tag

image-20240229191315999

# 菜单两层

ψ(`∇´)ψ

# 缺陷版

该示例中, 菜单一层和菜单二层的实现效果有一处相同的缺陷
当我点击 "客户列表"展示的页面里的"添加客户"这些非菜单权限功能时, "客户列表"的选中状态会消失!!

先来看看效果.

34

将菜单一层和菜单两层的关键代码进行比较!!

image-20240301162330596

二层菜单是渲染到下面这样的模版代码中的!!

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 完美版

我们要解决缺陷版的bug!!
明确需求: 点击非菜单权限时, 一级菜单默认展开,二级菜单默认选中!!

先来看看最终效果!

35

■ 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 做一个 归属/关联!
     具体操作:在权限表中实现<自关联>, 将非菜单权限跟菜单权限进行关联.

image-20240301171904873

具体如何实现呢? 在"缺陷版"菜单二层的代码上进行了更改, 更改的地方在截图中都进行了标注!!
精髓在于: 以前是通过url来判定默认选中、现在通过id来判定默认选中!!

image-20240301183121524


# 面包屑导航

路径导航 可以显示 权限之间的层级关系.

先来看看效果!

36

主要是对登陆获取到的权限信息进行一点处理!

  ■ 原来:
  [
    {'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/'
    }
  ]
1
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

在已有代码的基础上, 修改的地方在截图中都进行了标注!!

image-20240301191817928


# 权限粒度-按钮

将权限粒度控制到按钮级别!! ★ 关键在于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":..}
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Step3: 在模版页面添加判断条件 用的filter

看代码截图,其实就改了下 权限信息的数据结构, 其余的都没咋动!!

image-20240301205329283


张sir的部署
菜单和权限管理

← 张sir的部署 菜单和权限管理→

最近更新
01
deepseek本地部署+知识库
02-17
02
实操-微信小程序
02-14
03
教学-cursor深度探讨
02-13
更多文章>
Theme by Vdoing | Copyright © 2023-2025 DC | One Piece
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式