
正文
百万年薪python之路 -- RBAC角色权限设计
提示:扫一扫查出行【扫一扫了解最新限行尾号】
复制提示
RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。
在这种模型中,用户与角色之间,角色与权限之间,一般都是多对多的关系。
角色是什么?可以理解为一定数量的权限的集合,权限的载体。例如:一个论坛系统,“超级管理员”、“版主”都是角色。版主可管理版内的帖子、可管理版内的用户等,这些是权限。要给某个用户授予这些权限,不需要直接将权限授予用户,可将“版主”这个角色赋予该用户。
当用户的数量非常大时,要给系统每个用户逐一授权(授角色),是件非常烦琐的事情。这时,就需要给用户分组,每个用户组内有多个用户。除了可给用户授权外,还可以给用户组授权。这样一来,用户拥有的所有权限,就是用户个人拥有的权限与该用户所在用户组拥有的权限之和。
在应用系统中,权限表现成什么?对功能模块的操作,对上传文件的删改,菜单的访问,甚至页面上某个按钮、某个图片的可见性控制,都可属于权限的范畴。有些权限设计,会把功能操作作为一类,而把文件、菜单、页面元素等作为另一类,这样构成“用户-角色-权限-资源”的授权模型。而在做数据表建模时,可把功能操作和资源统一管理,也就是都直接与权限表进行关联,这样可能更具便捷性和易扩展性。
RBAC权限设计:
一张图搞定RBAC用户权限设计:
权限设计的大致步骤:
# 1 权限表设计(model)# 2 权限分配(数据分配)# 3 查询权限并注入权限(封装的session功能)# 4 权限验证(中间件)# 5 动态生成左侧菜单
CRM的权限设计1. 权限表设计(model)
# 用户表class UserInfo(models.Model): username = models.CharField(max_length=32) # 用户名 password = models.CharField(max_length=32) # 密码 最好在注册时把密码加密,在这里没有 roles = models.ManyToManyField('Role') # 关联到角色表,在数据库中会生成第三张表rbac_userinfo_roles表 def __str__(self): return self.username# 角色表class Role(models.Model): name = models.CharField(max_length=16) # 角色名 permissions = models.ManyToManyField('Permission') # 关联到权限表 def __str__(self): return self.name# 权限class Permission(models.Model): title = models.CharField(max_length=32) # 权限名,如 账单管理 url = models.CharField(max_length=32) # 权限的url menus = models.ForeignKey('Menu',null=True,blank=True) # 关联Permission表,用于 非菜单权限 关联 二级菜单权限 parent = models.ForeignKey('self',null=True,blank=True) # url_alias_name 存储url的别名 url_alias_name = models.CharField(max_length=32,null=True,blank=True) def __str__(self): return self.title# 一级菜单数据表class Menu(models.Model): name = models.CharField(max_length=32) # 一级菜单名 icon = models.CharField(max_length=32, null=True, blank=True) # 一级菜单所用的font-awesome图标值 weight = models.IntegerField(default=100) # 控制菜单排序的,权重值越大,菜单展示越靠前 def __str__(self): return self.name
# 一级菜单的核心思想 """ 一级菜单 id name icon 1 业务系统 2 教务系统 权限表 id title url menu_id 1 客户展示 /list/ 1 2 客户添加 /add/ None 3 跟进记录展示 /plist/ 1 4 课程记录 /course/ 2 5 课程记录添加 /add/course/ None """
2. 权限分配(数据分配)
rbac_permission表

rbac_role表

rbac_role_permissions表

rbac_userinfo表

rbac_userinfo_roles表

rbac_menu表

3. 查询权限并注入权限(封装的session功能)
from rbac import models# 权限注入到session中def init_permission(request, user_obj): # 登录成功之后,将该用户的所有权限(url)全部加入到session中 permission_list = models.Role.objects.filter( userinfo__username=user_obj.username ).values( 'permissions__url', # 需要设置权限的url 'permissions__title', # 权限的名称(即二级菜单名) 'permissions__pk', # 权限表里对应的主键值(id) 'permissions__menus__pk', # 一级菜单的主键值(id) 'permissions__menus__name', # 一级菜单名 'permissions__menus__icon', # 一级菜单的图标的font-awesome值 'permissions__menus__weight',# 一级菜单的权重(用来对多个一级菜单的排序) 'permissions__parent_id', # 权限的id(即二级菜单的id值) 'permissions__url_alias_name'# 权限的url的别名 ).distinct() # 相同的权限去重 # queryset对象不能通过Json进行可序列化,所以转化成List对象 # # Object of type 'QuerySet' is not JSON serializable # request.session['permission_list'] = list(permission_list) permission_dict = {} url_alias_name = [] # 存放所有的权限url别名 # 筛选菜单权限 menu_dict = {} for i in permission_list: permission_dict[i.get('permissions__pk')] = i url_alias_name.append(i.get('permissions__url_alias_name')) if i.get('permissions__menus__pk'): if i.get('permissions__menus__pk') in menu_dict: menu_dict[i.get('permissions__menus__pk')]['children'].append( { 'title': i.get('permissions__title'), 'url': i.get('permissions__url'), 'second_menu_id': i.get('permissions__pk'), } ) else: menu_dict[i.get('permissions__menus__pk')] = { 'name': i.get('permissions__menus__name'), 'icon': i.get('permissions__menus__icon'), 'weight': i.get('permissions__menus__weight'), 'children': [ { 'title': i.get('permissions__title'), 'url': i.get('permissions__url'), 'second_menu_id': i.get('permissions__pk'), } ] } # 将菜单权限注入到session request.session['menu_dict'] = menu_dict request.session['url_alias_name'] = url_alias_name request.session['permission_dict'] = permission_dict # menu_dict形成以下的数据结构 ''' { 1: { 'name': '业务系统', 'icon': 'fa fa-home fa-fw', 'weight': 100, 'children': [{ 'title': '客户管理', 'url': '/customer/list/', 'second_menu_id': None, }] }, 2: { 'name': '财务系统', 'icon': 'fa fa-jpy fa-fw', 'weight': 200, 'children': [{ 'title': '账单管理', 'url': '/payment/list/', 'second_menu_id': None, }] } } ''' """ # permission_dict 形成以下的数据结构 { 1: { 'permissions__url': '/customer/list/', 'permissions__title': '客户管理', 'permissions__pk': 1, 'permissions__menus__pk': 2, 'permissions__menus__name': '业务系统', 'permissions__menus__icon': 'fafa-homefa-fw', 'permissions__menus__weight': 200, 'permissions__parent_id': None, 'permissions__url_alias_name': 'customer_list' }, 2: { 'permissions__url': '/customer/add/', 'permissions__title': '添加客户', 'permissions__pk': 2, 'permissions__menus__pk': None, 'permissions__menus__name': None, 'permissions__menus__icon': None, 'permissions__menus__weight': None, 'permissions__parent_id': 1, 'permissions__url_alias_name': 'customer_add' }, 3: { 'permissions__url': '/customer/edit/(?P<cid>\\d+)/', 'permissions__title': '编辑客户', 'permissions__pk': 3, 'permissions__menus__pk': None, 'permissions__menus__name': None, 'permissions__menus__icon': None, 'permissions__menus__weight': None, 'permissions__parent_id': 1, 'permissions__url_alias_name': 'customer_edit' }, 4: { 'permissions__url': '/customer/del/(?P<cid>\\d+)/', 'permissions__title': '删除客户', 'permissions__pk': 4, 'permissions__menus__pk': None, 'permissions__menus__name': None, 'permissions__menus__icon': None, 'permissions__menus__weight': None, 'permissions__parent_id': 1, 'permissions__url_alias_name': 'customer_del' }, 5: { 'permissions__url': '/payment/list/', 'permissions__title': '账单管理', 'permissions__pk': 5, 'permissions__menus__pk': 1, 'permissions__menus__name': '财务系统', 'permissions__menus__icon': 'fafa-rmbfa-fw', 'permissions__menus__weight': 100, 'permissions__parent_id': None, 'permissions__url_alias_name': 'payment_list' }, 6: { 'permissions__url': '/payment/add/', 'permissions__title': '添加缴费', 'permissions__pk': 6, 'permissions__menus__pk': None, 'permissions__menus__name': None, 'permissions__menus__icon': None, 'permissions__menus__weight': None, 'permissions__parent_id': 5, 'permissions__url_alias_name': 'payment_add' }, 7: { 'permissions__url': '/payment/edit/(?P<pid>\\d+)/', 'permissions__title': '编辑缴费', 'permissions__pk': 7, 'permissions__menus__pk': None, 'permissions__menus__name': None, 'permissions__menus__icon': None, 'permissions__menus__weight': None, 'permissions__parent_id': 5, 'permissions__url_alias_name': 'payment_edit' }, 8: { 'permissions__url': '/payment/del/(?P<pid>\\d+)/', 'permissions__title': '删除缴费', 'permissions__pk': 8, 'permissions__menus__pk': None, 'permissions__menus__name': None, 'permissions__menus__icon': None, 'permissions__menus__weight': None, 'permissions__parent_id': 5, 'permissions__url_alias_name': 'payment_del' }, 9: { 'permissions__url': '/nashui/', 'permissions__title': '纳税管理', 'permissions__pk': 9, 'permissions__menus__pk': 1, 'permissions__menus__name': '财务系统', 'permissions__menus__icon': 'fafa-rmbfa-fw', 'permissions__menus__weight': 100, 'permissions__parent_id': None, 'permissions__url_alias_name': 'nashui' }} """
4. 权限验证(中间件)
在rbac应用里新建一个middlewares文件夹(文件夹名随意),然后新建一个mymiddleware.py文件(py文件名字随意)
记得在settings文件的MIDDLEWARE里加入中间件
import refrom django.utils.deprecation import MiddlewareMixinfrom django.urls import reversefrom django.shortcuts import redirect, HttpResponse, renderclass Auth(MiddlewareMixin): def process_request(self, request): # 登录认证白名单 white_list = [reverse('login'), reverse('logout'), ] # 权限认证白名单 permission_white_list = [reverse('index'), '/admin/*'] request.pid = None bread_crumb = [ {'url': reverse('index'), 'title': '首页'} ] request.bread_crumb = bread_crumb # 把生成面包屑的数据放入到request对象中,在42行处加入注入数据 # 登录认证 path = request.path if path not in white_list: is_login = request.session.get('is_login') if not is_login: return redirect('login') # 权限认证 permission_dict = request.session.get('permission_dict') for white_path in permission_white_list: if re.match(white_path, path): break else: for i in permission_dict.values(): reg = r"^%s$" % i['permissions__url'] if re.match(reg, path): pid = i.get('permissions__parent_id') if pid: # 如果这个不是菜单权限,就执行 # 父级二级菜单路径信息 request.bread_crumb.append( { 'url': permission_dict[str(pid)]['permissions__url'], # 面包屑的父级二级菜单url 'title': permission_dict[str(pid)]['permissions__title'] # 面包屑的父级二级菜单名字 } ) # 子权限的路径信息 #/payment/add/ request.bread_crumb.append( { # 面包屑的当前权限url 'url': i.get('permissions__url'), # 面包屑的当前权限名字 'title': i.get('permissions__title') } ) request.pid = pid # 把二级菜单的id注入到request里 else: # 二级菜单路径信息 request.bread_crumb.append( { 'url': i.get('permissions__url'), 'title': i.get('permissions__title') } ) request.pid = i.get('permissions__pk') break else: return HttpResponse('你权限不足!!!')
5. 动态生成左侧菜单
采用自定义标签来完成
- 现在rbac里新建一个templatetags文件夹(文件夹名字必须叫这个)
- 在文件夹里新建一个mytags.py文件(py文件名字任意)
- 在py文件里注册 "注册器",如下
from django import templateregister = template.Library()
- 在函数上添加 @register.inclusion_tag('HTML文件名')
mytags文件:
import refrom collections import OrderedDictfrom django import templateregister = template.Library() # 注册 注册器register@register.inclusion_tag('menu.html')def menu(request): menu_dict = request.session.get('menu_dict') menu_order_key = sorted(menu_dict, key=lambda x: menu_dict[x]['weight'], reverse=True) # 对menu_dict里字典数据排序, menu_order_dict = OrderedDict() # 生成有序字典,python3.6以上字典默认顺序为加入字典时的顺序,可不用OrderedDict for key in menu_order_key: menu_order_dict[key] = menu_dict[key] # path = request.path for k, v in menu_order_dict.items(): v['class'] = 'hidden' for i in v['children']: # if re.match(i['url'], path): if request.pid == i['second_menu_id']: v['class'] = '' i['class'] = 'active' menu_data = {'menu_data': menu_order_dict} return menu_data
生成左侧菜单的HTML文件
layout.html的核心部分 :
#### HTML中生成左侧菜单的两行代码{% load mytags %}{% menu request %}##### 面包屑的代码<div> <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">{# <li><a href="#">首页</a></li>#}{# <li class="active">客户管理</li>#} {% for crumb in request.bread_crumb %} {% if forloop.last %} <li class="active">{{ crumb.title }}</li> {% else %} <li><a href="{{ crumb.url }}">{{ crumb.title }}</a></li> {% endif %} {% endfor %} </ol></div>






