CRM【第一篇】: 权限组件之权限控制
1. 问:为什么程序需要权限控制?
答:生活中的权限限制,① 看灾难片电影《2012》中富人和权贵有权登上诺亚方舟,穷苦老百姓只有等着灾难的来临;② 屌丝们,有没有想过为什么那些长得漂亮身材好的姑娘在你身边不存在呢?因为有钱人和漂亮姑娘都是珍贵稀有的,稀有的人在一起玩耍和解锁各种姿势。而你,无权拥有他们,只能自己玩自己了。
程序开发时的权限控制,对于不同用户使用系统时候就应该有不同的功能,如:
- 普通员工
- 部门主管
- 总监
- 总裁
所以,只要有不同角色的人员来使用系统,那么就肯定需要权限系统。
2. 问:为什么要开发权限组件?
答:假设你今年25岁,从今天开始写代码到80岁,每年写5个项目,那么你的一生就会写275个项目,保守估计其中应该有150+个都需要用到权限控制,为了以后不再重复的写代码,所以就开发一个权限组件以便之后55年的岁月中使用。 亲,不要太较真哦,你觉得程序员能到80岁么,哈哈哈哈哈哈哈
偷偷告诉你:老程序员开发速度快,其中一个原因是经验丰富,另外一个就是他自己保留了很多组件,新系统开发时,只需把组件拼凑起来基本就可以完成。
3. 问:web开发中权限指的是什么?
答:web程序是通过 url 的切换来查看不同的页面(功能),所以权限指的其实就是URL,对url控制就是对权限的控制。
结论:一个人有多少个权限就取决于他有多少个URL的访问权限。
权限表结构设计:第一版
问答环节中已得出权限就是URL的结论,那么就可以开始设计表结构了。
- 一个用户可以有多个权限。
- 一个权限可以分配给多个用户。
你设计的表结构大概会是这个样子:
现在,此时此刻是不是觉得自己设计出的表结构棒棒哒!!!
But,无论是是否承认,你还是too young too native,因为老汉腚眼一看就有问题....
问题:假设 “老男孩”和“Alex” 这俩货都是老板,老板的权限一定是非常多。那么试想,如果给这俩货分配权限时需要在【用户权限关系表中】添加好多条数据。假设再次需要对老板的权限进行修改时,又需要在【用户权限关系表】中找到这俩人所有的数据进行更新,太他妈烦了吧!!! 类似的,如果给其他相同角色的人来分配权限时,必然会非常繁琐。
权限表结构设计:第二版
聪明机智的一定在上述的表述中看出了写门道,如果对用户进行角色的划分,然后对角色进行权限的分配,这不就迎刃而解了么。
- 一个人可以有多个角色。
- 一个角色可以有多个人。
- 一个角色可以有多个权限。
- 一个权限可以分配给多个角色。
表结构设计:
这次调整之后,由原来的【基于用户的权限控制】转换成【基于角色的权限控制】,以后再进行分配权限时只需要给指定角色分配一次权限,给众多用户再次分配指定角色即可。

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
小伙子,告诉你一个事实,不经意间,你居然设计出了一个经典的权限访问控制系统:rbac(Role-Based Access Control)基于角色的权限访问控制。你这么优秀,为什么不来老男孩IT教育?路飞学城也行呀! 哈哈哈哈。
注意:现在的设计还不是最终版,但之后的设计都是在此版本基础上扩增的,为了让大家能够更好的理解,我们暂且再此基础上继续开发,直到遇到无法满足的情况,再进行整改。
源码示例:猛击下载
客户管理之权限控制
学习知识最好的方式就是试错,坑踩多了那么学到的知识自然而然就多了,所以接下里下来我们用《客户管理》系统为示例,提出功能并实现,并且随着功能越来越多,一点点来找出问题,并解决问题。
目录结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | luffy_permission/ ├── db.sqlite3 ├── luffy_permission │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── rbac # 权限组件,便于以后应用到其他系统 │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── tests.py │ └── views.py ├── templates └── web # 客户管理业务 ├── __init__.py ├── admin.py ├── apps.py ├── models.py ├── tests.py └── views.py |

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

from django.db import models class Customer(models.Model): """ 客户表 """ name = models.CharField(verbose_name='姓名', max_length=32) age = models.CharField(verbose_name='年龄', max_length=32) email = models.EmailField(verbose_name='邮箱', max_length=32) company = models.CharField(verbose_name='公司', max_length=32) class Payment(models.Model): """ 付费记录 """ customer = models.ForeignKey(verbose_name='关联客户', to='Customer') money = models.IntegerField(verbose_name='付费金额') create_time = models.DateTimeField(verbose_name='付费时间', auto_now_add=True)
《客户管理》系统截图:基本增删改查和Excel导入源码下载:猛击这里
以上简易版客户管理系统中的URL有:
- 客户管理
- 客户列表:/customer/list/
- 添加客户:/customer/add/
- 删除客户:/customer/list/(?P<cid>\d+)/
- 修改客户:/customer/edit/(?P<cid>\d+)/
- 批量导入:/customer/import/
- 下载模板:/customer/tpl/
- 账单管理
- 账单列表:/payment/list/
- 添加账单:/payment/add/
- 删除账单:/payment/del/(?P<pid>\d+)/
- 修改账单:/payment/edit/<?P<pid>\d+/
那么接下来,我们就在权限组件中录入相关信息:
- 录入权限
- 创建用户
- 创建角色
- 用户分配角色
- 角色分配权限
这么一来,用户登录时,就可以根据自己的【用户】找到所有的角色,再根据角色找到所有的权限,再将权限信息放入session,以后每次访问时候需要先去session检查是否有权访问。
已录入权限数据源码下载:猛击这里
含用户登录权限源码下载:猛击这里(简易版)
含用户登录权限源码下载:猛击这里
至此,基本的权限控制已经完成,基本流程为:
- 用户登录,获取权限信息并放入session
- 用户访问,在中间件从session中获取用户权限信息,并进行权限验证。
所有示例中的账户信息:
1 2 3 4 5 6 7 | 账户一: 用户名:alex 密码: 123 账户二: 用户名:wupeiqi 密码: 123 |
客户管理之动态“一级”菜单
上述过程中的菜单是在程序中定义好的,无法根据用户权限进行动态配置。
那么,接下来我们来完成一个 “单级菜单”的功能:

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) icon = models.CharField(verbose_name='图标', max_length=32, null=True, blank=True, help_text='菜单才设置图标') is_menu = models.BooleanField(verbose_name='是否是菜单', default=False) 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
这样可以开发出一个单级菜单的示例:
示例源码下载:猛击这里(无默认选中)
示例源码下载:猛击这里(含默认选中)
客户管理之动态“二级”菜单
对于功能比较少的应用程序 “一级菜单” 基本可以满足需求,但是功能多的程序就需要 “二级菜单” 了,并且访问时候需要默认选中指定菜单。

from django.db import models class Menu(models.Model): """ 菜单 """ title = models.CharField(verbose_name='菜单', max_length=32) icon = models.CharField(verbose_name='图标', max_length=32) def __str__(self): return self.title class Permission(models.Model): """ 权限表 """ title = models.CharField(verbose_name='标题', max_length=32) url = models.CharField(verbose_name='含正则的URL', max_length=128) menu = models.ForeignKey(verbose_name='菜单', to='Menu', null=True, blank=True, help_text='null表示非菜单') 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
示例效果:
示例源码下载:猛击这里
示例源码下载:猛击这里(路飞线上录制代码示例)
客户管理之默认展开非菜单URL
由于很多URL都是不能作为菜单,所以当点击该类功能时,是无法默认展开菜单的,如:
- 删除
- 修改
- ...
此类页面只能通过菜单页面二次点击才能跳转,此时也应该为他们默认展开原菜单。

from django.db import models class Menu(models.Model): """ 菜单 """ title = models.CharField(verbose_name='菜单', max_length=32) icon = models.CharField(verbose_name='图标', max_length=32) def __str__(self): return self.title class Permission(models.Model): """ 权限表 """ title = models.CharField(verbose_name='标题', max_length=32) url = models.CharField(verbose_name='含正则的URL', max_length=128) pid = models.ForeignKey(verbose_name='默认选中权限', to='Permission', related_name='ps', null=True, blank=True, help_text="对于无法作为菜单的URL,可以为其选择一个可以作为菜单的权限,那么访问时,则默认选中此权限", limit_choices_to={'menu__isnull': False}) menu = models.ForeignKey(verbose_name='菜单', to='Menu', null=True, blank=True, help_text='null表示非菜单') 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. django formset示例
源码示例下载:猛击这里(django formset实现批量添加和编辑)
6. 批量操作权限的显示
源码示例下载:猛击这里(批量操作权限的显示)
7. 批量操作权限:添加、删除、更新
源码示例下载:猛击这里(权限的批量增删改)
8. 权限分配
源码示例下载:猛击这里(权限分配)
RBAC组件应用及文档
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | """ 1. 将rbac组件拷贝项目。 2. 将rbac/migrations目录中的数据库迁移记录删除 3. 业务系统中用户表结构的设计 业务表结构中的用户表需要和rbac中的用户有继承关系,如: rbac/models.py 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) 严重提醒 Role 不要加引号 def __str__(self): return self.name class Meta: # django以后再做数据库迁移时,不再为UserInfo类创建相关的表以及表结构了。 # 此类可以当做"父类",被其他Model类继承。 abstract = True 业务/models.py 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') 4. 讲业务系统中的用户表的路径写到配置文件。 # 业务中的用户表 RBAC_USER_MODLE_CLASS = "app01.models.UserInfo" 用于在rbac分配权限时,读取业务表中的用户信息。 5. 业务逻辑开发 将所有的路由都设置一个name,如: url(r'^login/$', account.login, name='login'), url(r'^logout/$', account.logout, name='logout'), url(r'^index/$', account.index, name='index'), url(r'^user/list/$', user.user_list, name='user_list'), url(r'^user/add/$', user.user_add, name='user_add'), url(r'^user/edit/(?P<pk>\d+)/$', user.user_edit, name='user_edit'), url(r'^user/del/(?P<pk>\d+)/$', user.user_del, name='user_del'), url(r'^user/reset/password/(?P<pk>\d+)/$', user.user_reset_pwd, name='user_reset_pwd'), 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'), 用于反向生成URL以及粒度控制到按钮级别的权限控制。 6. 权限信息录入 - 在url中添加rbac的路由分发,注意:必须设置namespace urlpatterns = [ ... url(r'^rbac/', include('rbac.urls', namespace='rbac')), ] - rbac提供的地址进行操作 - http://127.0.0.1:8000/rbac/menu/list/ - http://127.0.0.1:8000/rbac/role/list/ - http://127.0.0.1:8000/rbac/distribute/permissions/ 相关配置:自动发现URL时,排除的URL: # 自动化发现路由中URL时,排除的URL AUTO_DISCOVER_EXCLUDE = [ '/admin/.*', '/login/', '/logout/', '/index/', ] 7. 编写用户登录的逻辑【进行权限初始化】 from django.shortcuts import render, redirect from app01 import models from rbac.service.init_permission import init_permission def login(request): if request.method == 'GET': return render(request, 'login.html') user = request.POST.get('username') pwd = request.POST.get('password') user_object = models.UserInfo.objects.filter(name=user, password=pwd).first() if not user_object: return render(request, 'login.html', {'error': '用户名或密码错误'}) # 用户权限信息的初始化 init_permission(user_object, request) return redirect('/index/') 相关配置: 权限和菜单的session key: setting.py PERMISSION_SESSION_KEY = "luffy_permission_url_list_key" MENU_SESSION_KEY = "luffy_permission_menu_key" 8. 编写一个首页的逻辑 def index(request): return render(request, 'index.html') 相关配置:需要登录但无需权限的URL # 需要登录但无需权限的URL NO_PERMISSION_LIST = [ '/index/', '/logout/', ] 9. 通过中间件进行权限校验 # 权限校验 MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'rbac.middlewares.rbac.RbacMiddleware', ] # 白名单,无需登录就可以访问 VALID_URL_LIST = [ '/login/', '/admin/.*' ] 10. 粒度到按钮级别的控制 {% extends 'layout.html' %} {% load rbac %} {% block content %} <div class="luffy-container"> <div class="btn-group" style="margin: 5px 0"> {% if request|has_permission:'host_add' %} <a class="btn btn-default" href="{% memory_url request 'host_add' %}"> <i class="fa fa-plus-square" aria-hidden="true"></i> 添加主机 </a> {% endif %} </div> <table class="table table-bordered table-hover"> <thead> <tr> <th>主机名</th> <th>IP</th> <th>部门</th> {% if request|has_permission:'host_edit' or request|has_permission:'host_del' %} <th>操作</th> {% endif %} </tr> </thead> <tbody> {% for row in host_queryset %} <tr> <td>{{ row.hostname }}</td> <td>{{ row.ip }}</td> <td>{{ row.depart.title }}</td> {% if request|has_permission:'host_edit' or request|has_permission:'host_del' %} <td> {% if request|has_permission:'host_edit' %} <a style="color: #333333;" href="{% memory_url request 'host_edit' pk=row.id %}"> <i class="fa fa-edit" aria-hidden="true"></i></a> {% endif %} {% if request|has_permission:'host_del' %} <a style="color: #d9534f;" href="{% memory_url request 'host_del' pk=row.id %}"><i class="fa fa-trash-o"></i></a> {% endif %} </td> {% endif %} </tr> {% endfor %} </tbody> </table> </div> {% endblock %} 总结,目的是希望在任意系统中应用权限系统。 - 用户登录 + 用户首页 + 用户注销 业务逻辑 - 项目业务逻辑开发 注意:开发时候灵活的去设置layout.html中的两个inclusion_tag <div class="pg-body"> <div class="left-menu"> <div class="menu-body"> {% multi_menu request %} # 开发时,去掉;上下时,取回。 </div> </div> <div class="right-body"> <div> {% breadcrumb request %} # 开发时,去掉;上下时,取回。 </div> {% block content %} {% endblock %} </div> </div> - 权限信息的录入 - 配置文件 # 注册APP INSTALLED_APPS = [ ... 'rbac.apps.RbacConfig' ] # 应用中间件 MIDDLEWARE = [ ... 'rbac.middlewares.rbac.RbacMiddleware', ] # 业务中的用户表 RBAC_USER_MODLE_CLASS = "app01.models.UserInfo" # 权限在Session中存储的key PERMISSION_SESSION_KEY = "luffy_permission_url_list_key" # 菜单在Session中存储的key MENU_SESSION_KEY = "luffy_permission_menu_key" # 白名单 VALID_URL_LIST = [ '/login/', '/admin/.*' ] # 需要登录但无需权限的URL NO_PERMISSION_LIST = [ '/index/', '/logout/', ] # 自动化发现路由中URL时,排除的URL AUTO_DISCOVER_EXCLUDE = [ '/admin/.*', '/login/', '/logout/', '/index/', ] - 粒度到按钮级别的控制 """ |
最终版组件下载:rbac.zip
源码示例下载:rbac组件应用之主机管理系统【auto_luffy.zip】

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探