角色权限管理组件
角色权限管理:role basic access control,简称rbac。
权限管理: 表: 用户表 角色表 用户角色关系表 菜单表 权限表 操作表 角色权限操作关系表 功能: - 权限控制 - 动态菜单 原理: - 权限控制 a. 登录时,获取当前用户所有权限,放置在Session中 request.session['rbac_permission_session_key'] = 'asdf' { '/index.html': [G,P...] } b. 其他请求,在session中检测是否具有权限 中间件 + 配置文件 - 动态菜单 a. 登录时,获取当前用户菜单和权限,放置在Session中 b. 其他请求,在session中获取菜单,动态生成(simple_tag)
数据库表设计
from django.db import models class User(models.Model): """ 用户表 """ username = models.CharField(verbose_name='用户名', max_length=32) password = models.CharField(verbose_name='密码', max_length=64) email = models.EmailField(verbose_name='邮箱') def __str__(self): return self.username class Role(models.Model): """ 角色表 """ caption = models.CharField(verbose_name='角色', max_length=32) def __str__(self): return self.caption class User2Role(models.Model): """ 用户角色关系表 """ user = models.ForeignKey(User, verbose_name='用户', related_name='roles') role = models.ForeignKey(Role, verbose_name='角色', related_name='users') def __str__(self): return '%s-%s' % (self.user.username, self.role.caption,) class Menu(models.Model): """ 菜单表 """ caption = models.CharField(verbose_name='菜单名称', max_length=32) parent = models.ForeignKey('self', verbose_name='父菜单', related_name='p', null=True, blank=True) def __str__(self): prev = "" parent = self.parent while True: if parent: prev = prev + '-' + str(parent.caption) parent = parent.parent else: break return '%s-%s' % (prev, self.caption,) class Permission(models.Model): """ 权限 """ caption = models.CharField(verbose_name='权限', max_length=32) url = models.CharField(verbose_name='URL正则', max_length=128) menu = models.ForeignKey(Menu, verbose_name='所属菜单', related_name='permissions',null=True,blank=True) def __str__(self): return "%s-%s" % (self.caption, self.url,) class Action(models.Model): """ 操作:增删改查 """ caption = models.CharField(verbose_name='操作标题', max_length=32) code = models.CharField(verbose_name='方法', max_length=32) def __str__(self): return self.caption class Permission2Action2Role(models.Model): """ 权限操作关系表 """ permission = models.ForeignKey(Permission, verbose_name='权限URL', related_name='actions') action = models.ForeignKey(Action, verbose_name='操作', related_name='permissions') role = models.ForeignKey(Role, verbose_name='角色', related_name='p2as') class Meta: unique_together = ( ('permission', 'action', 'role'), ) def __str__(self): return "%s-%s-%s" % (self.permission, self.action, self.role,)
获取用户权限以及中间件权限控制
URL路由系统 #urls.py from app02 import views as views2 url(r'^auto-login.html$', views2.login), url(r'^auto-index.html$', views2.index), 中间件 #settings.py 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', 'middleware.md.M1', ] 配置中间件 #md.py from django.utils.deprecation import MiddlewareMixin from django.shortcuts import HttpResponse import re class M1(MiddlewareMixin): def process_request(self,request,*args,**kwargs): valid = ['/auth-login.html','/auth-index.html'] if request.path_info not in valid: action = request.GET.get('md') # GET user_permission_dict = request.session.get('user_permission_dict') if not user_permission_dict: return HttpResponse('无权限') # action_list = user_permission_dict.get(request.path_info) flag = False for k,v in user_permission_dict.items(): if re.match(k,request.path_info): if action in v: flag = True break if not flag: return HttpResponse('无权限') 视图函数 #views.py def login(request): if request.method == "GET": return render(request,'login2.html') else: user_permission_dict = { '/ah-index.html/':["GET","POST","DEL","Edit"], '/order.html/':["GET","POST","DEL","Edit"], '/index-(\d+).html/':["GET","POST","DEL","Edit"], } request.session['user_permission_dict'] = user_permission_dict return HttpResponse('登录成功') def index(request): # http://127.0.0.1:8000/auth-index.html?md=GET return HttpResponse('登陆,并且有权限才能看见我')
输出动态菜单
urlpatterns = [ url(r'^app02_test.html', views2.app02_test), ]
from django.contrib import admin # Register your models here. from app02 import models admin.site.register(models.User) admin.site.register(models.Role) admin.site.register(models.User2Role) admin.site.register(models.Menu) admin.site.register(models.Permission) admin.site.register(models.Action) admin.site.register(models.Permission2Action2Role)
def app02_test(request): """ 需要用户名或用户ID,产出:用户关联所有菜单 :param request: :return: """ # 所有菜单:处理成当前用关联的菜单 all_menu_list = models.Menu.objects.all().values('id', 'caption', 'parent_id') """ [ {'id':1, 'caption':'菜单1', parent_id:None}, {'id':2, 'caption':'菜单2', parent_id:None}, {'id':3, 'caption':'菜单3', parent_id:None}, {'id':4, 'caption':'菜单1-1', parent_id:1}, ] { 1:{'id':1, 'caption':'菜单1', parent_id:None,status:False,opened:False,child:[]}, 2:{'id':2, 'caption':'菜单2', parent_id:None,status:False,opened:False,child:[]}, 3:{'id':3, 'caption':'菜单3', parent_id:None,status:False,opened:False,child:[]}, 5:{'id':4, 'caption':'菜单1-1', parent_id:1,status:False,opened:False,child:[]}, } """ user = models.User.objects.filter(username='alex').first() role_list = models.Role.objects.filter(users__user=user) permission_list = models.Permission2Action2Role.objects.filter(role__in=role_list).values('permission__id', 'permission__url', 'permission__menu_id', 'permission__caption').distinct() """ [ {'permission__url':'/order.html','permission__caption': '订单管理','permission__menu_id': 1 }, {'permission__url':'/order.html','permission__caption': '订单管理','permission__menu_id': 2 }, {'permission__url':'/order.html','permission__caption': '订单管理','permission__menu_id': 3 }, {'permission__url':'/order.html','permission__caption': '订单管理','permission__menu_id': 4 }, ] """ ##### 将权限挂靠到菜单上 ######## all_menu_dict = {} for row in all_menu_list: row['child'] = [] # 添加孩子 row['status'] = False # 是否显示菜单 row['opened'] = False # 是否默认打开 all_menu_dict[row['id']] = row for per in permission_list: if not per['permission__menu_id']: continue item = { 'id': per['permission__id'], 'caption': per['permission__caption'], 'parent_id': per['permission__menu_id'], 'url': per['permission__url'], 'status': True, 'opened': False } # print(item["url"]) if re.match(per['permission__url'],request.path_info): # if re.match(per['permission__url'], "/orders.html"): item['opened'] = True pid = item['parent_id'] all_menu_dict[pid]['child'].append(item) # 将当前权限前辈status=True temp = pid # 1.父亲ID while not all_menu_dict[temp]['status']: all_menu_dict[temp]['status'] = True temp = all_menu_dict[temp]['parent_id'] if not temp: break # 将当前权限前辈opened=True if item['opened']: temp1 = pid # 1.父亲ID while not all_menu_dict[temp1]['opened']: all_menu_dict[temp1]['opened'] = True temp1 = all_menu_dict[temp1]['parent_id'] if not temp1: break # ############ 处理菜单和菜单之间的等级关系 ############ """ all_menu_dict = { 1:{'id':1, 'caption':'菜单1', parent_id:None,status:False,opened:False,child:[{'permission__url':'/order.html','permission__caption': '订单管理','permission__menu_id': 1 },]}, 2:{'id':2, 'caption':'菜单2', parent_id:None,status:False,opened:False,child:[]}, 3:{'id':3, 'caption':'菜单3', parent_id:None,status:False,opened:False,child:[]}, 5:{'id':4, 'caption':'菜单1-1', parent_id:1,status:False,opened:False,child:[]}, } all_menu_list= [ {'id':1, 'caption':'菜单1', parent_id:None,status:False,opened:False,child:[{'permission__url':'/order.html','permission__caption': '订单管理','permission__menu_id': 1 }, {'id':4, 'caption':'菜单1-1', parent_id:1,status:False,opened:False,child:[]},]}, {'id':2, 'caption':'菜单2', parent_id:None,status:False,opened:False,child:[]}, {'id':3, 'caption':'菜单3', parent_id:None,status:False,opened:False,child:[]}, ] """ result = [] for row in all_menu_list: pid = row['parent_id'] if pid: all_menu_dict[pid]['child'].append(row) else: result.append(row) ##################### 结构化处理结果 ##################### # print(result) # for row in result: # # print(row['caption'], row['status'], row['opened'], ) # print(row) ##################### 通过结构化处理结果,生成菜单开始 ##################### def menu_tree(menu_list): tpl1 = """ <div class='menu-item'> <div class='menu-header'>{0}</div> <div class='menu-body {2}'>{1}</div> </div> """ tpl2 = """ <a href='{0}' class='{1}'>{2}</a> """ menu_str = "" for menu in menu_list: if not menu['status']: continue # menu: 菜单,权限(url) if menu.get('url'): # 权限 menu_str += tpl2.format(menu['url'],'active' if menu['opened'] else "",menu['caption']) else: # 菜单 if menu['child']: child_html = menu_tree(menu['child']) else: child_html = "" menu_str += tpl1.format(menu['caption'], child_html,"" if menu['opened'] else 'hide') return menu_str menu_html = menu_tree(result) return render(request, "menu_html.html", {"menu_html":menu_html})
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> .menu-body{ margin-left: 20px; } .menu-body a{ display: block; } .menu-body a.active{ color: red; } .hide{ display: none; } </style> </head> <body> {{ menu_html|safe }} <script src="/static/jquery-3.2.1.js"></script> <script> $(function () { $(".menu-header").click(function () { $(this).next().removeClass("hide").parent().siblings().find(".menu-body").addClass("hide") }) }) </script> </body> </html>
生成APP组件
生成公共app - 权限限制 - 生成菜单
python3 manage.py startapp rbac
#白名单url, 不验证 VALID_URL = [ '/app01/.*', '/app02/.*', '/login.html', '/logout.html', ]
from django.db import models class User(models.Model): """ 用户表 """ username = models.CharField(verbose_name='用户名', max_length=32) password = models.CharField(verbose_name='密码', max_length=64) email = models.EmailField(verbose_name='邮箱') def __str__(self): return self.username class Role(models.Model): """ 角色表 """ caption = models.CharField(verbose_name='角色', max_length=32) def __str__(self): return self.caption class User2Role(models.Model): """ 用户角色关系表 """ user = models.ForeignKey(User, verbose_name='用户', related_name='roles') role = models.ForeignKey(Role, verbose_name='角色', related_name='users') def __str__(self): return '%s-%s' % (self.user.username, self.role.caption,) class Menu(models.Model): """ 菜单表 """ caption = models.CharField(verbose_name='菜单名称', max_length=32) parent = models.ForeignKey('self', verbose_name='父菜单', related_name='p', null=True, blank=True) def __str__(self): prev = "" parent = self.parent while True: if parent: prev = prev + '-' + str(parent.caption) parent = parent.parent else: break return '%s-%s' % (prev, self.caption,) class Permission(models.Model): """ 权限 """ caption = models.CharField(verbose_name='权限', max_length=32) url = models.CharField(verbose_name='URL正则', max_length=128) menu = models.ForeignKey(Menu, verbose_name='所属菜单', related_name='permissions',null=True,blank=True) def __str__(self): return "%s-%s" % (self.caption, self.url,) class Action(models.Model): """ 操作:增删改查 """ caption = models.CharField(verbose_name='操作标题', max_length=32) code = models.CharField(verbose_name='方法', max_length=32) def __str__(self): return self.caption class Permission2Action2Role(models.Model): """ 权限操作关系表 """ permission = models.ForeignKey(Permission, verbose_name='权限URL', related_name='actions') action = models.ForeignKey(Action, verbose_name='操作', related_name='permissions') role = models.ForeignKey(Role, verbose_name='角色', related_name='p2as') class Meta: unique_together = ( ('permission', 'action', 'role'), ) def __str__(self): return "%s-%s-%s" % (self.permission, self.action, self.role,)
#验证中间件 from django.utils.deprecation import MiddlewareMixin from django.shortcuts import HttpResponse from rbac import config import re class RbacMiddleware(MiddlewareMixin): def process_request(self,request,*args,**kwargs): for pattern in config.VALID_URL: if re.match(pattern,request.path_info): return None action = request.GET.get('md') # GET user_permission_dict = request.session.get('user_permission_dict') if not user_permission_dict: return HttpResponse('无权限') # action_list = user_permission_dict.get(request.path_info) flag = False for k,v in user_permission_dict.items(): if re.match(k,request.path_info): if action in v: flag = True break if not flag: return HttpResponse('无权限')
import re from rbac import models from django.utils.safestring import mark_safe def permission_session(user_id,request): """ :param user_id: rbac中的user表中一条数据id :param request: :return: """ # obj = models.User.objects.filter(username='杨明').first() # # # x = models.User2Role.objects.filter(user_id=obj.id) # # [User2Role,User2Role,User2Role] # # role_list = models.Role.objects.filter(users__user_id=obj.id) # # [Role,] # from django.db.models import Count # # permission_list = models.Permission2Action2Role.objects.filter(role__in=role_list).values('permission__url','action__code').annotate(c=Count('id')) # permission_list = models.Permission2Action2Role.objects.filter(role__in=role_list).values('permission__url','action__code').distinct() """ [ {permission_url: '/index.html', action_code:'GET'}, {permission_url: '/index.html', action_code:'POST'}, {permission_url: '/index.html', action_code:'DEL'}, {permission_url: '/index.html', action_code:'Edit'}, {permission_url: '/order.html', action_code:'GET'}, {permission_url: '/order.html', action_code:'POST'}, {permission_url: '/order.html', action_code:'DEL'}, {permission_url: '/order.html', action_code:'Edit'}, ] 放在Session中 /index.html?md=GET { '/index.html': [GET,POST,DEL,Edit], '/order.html': [GET,POST,DEL,Edit], } """ user_permission_dict = { '/ah-index.html': ["GET","POST","DEL","Edit"], '/order.html': ["GET","POST","DEL","Edit"], '/index-(\d+).html': ["GET","POST","DEL","Edit"], } request.session['user_permission_dict'] = user_permission_dict def menu(user_id,current_url): """ 根据用户ID,当前URL:获取用户所有菜单以及权限,是否显示,是否打开 :param user_id: :param current_url: :return: """ # 所有菜单:处理成当前用关联的菜单 all_menu_list = models.Menu.objects.all().values('id','caption','parent_id') user = models.User.objects.filter(id=user_id).first() role_list = models.Role.objects.filter(users__user=user) permission_list = models.Permission2Action2Role.objects.filter(role__in=role_list).values('permission__id','permission__url','permission__menu_id','permission__caption').distinct() ##### 将权限挂靠到菜单上 ######## all_menu_dict = {} for row in all_menu_list: row['child'] = [] # 添加孩子 row['status'] = False # 是否显示菜单 row['opened'] = False # 是否默认打开 all_menu_dict[row['id']] = row for per in permission_list: if not per['permission__menu_id']: continue item = { 'id':per['permission__id'], 'caption':per['permission__caption'], 'parent_id':per['permission__menu_id'], 'url': per['permission__url'], 'status': True, 'opened': False } if re.match(per['permission__url'],current_url): item['opened'] = True pid = item['parent_id'] all_menu_dict[pid]['child'].append(item) # 将当前权限前辈status=True temp = pid # 1.父亲ID while not all_menu_dict[temp]['status']: all_menu_dict[temp]['status'] = True temp = all_menu_dict[temp]['parent_id'] if not temp: break # 将当前权限前辈opened=True if item['opened']: temp1 = pid # 1.父亲ID while not all_menu_dict[temp1]['opened']: all_menu_dict[temp1]['opened'] = True temp1 = all_menu_dict[temp1]['parent_id'] if not temp1: break # ############ 处理菜单和菜单之间的等级关系 ############ result = [] for row in all_menu_list: pid = row['parent_id'] if pid: all_menu_dict[pid]['child'].append(row) else: result.append(row) ##################### 结构化处理结果 ##################### for row in result: print(row['caption'],row['status'],row['opened'],row) def menu_tree(menu_list): tpl1 = """ <div class='menu-item'> <div class='menu-header'>{0}</div> <div class='menu-body {2}'>{1}</div> </div> """ tpl2 = """ <a href='{0}' class='{1}'>{2}</a> """ menu_str = "" for menu in menu_list: if not menu['status']: continue # menu: 菜单,权限(url) if menu.get('url'): # 权限 menu_str += tpl2.format(menu['url'],'active' if menu['opened'] else "",menu['caption']) else: # 菜单 if menu['child']: child_html = menu_tree(menu['child']) else: child_html = "" menu_str += tpl1.format(menu['caption'], child_html,"" if menu['opened'] else 'hide') return menu_str menu_html = menu_tree(result) return menu_html # simple_tag def css(): v = """ <style> .hide{ display: none; } .menu-body{ margin-left: 20px; } .menu-body a{ display: block; } .menu-body a.active{ color: red; } </style> """ return v # simple_tag def js(): v = """ <script> $(function(){ $('.menu-header').click(function(){ $(this).next().removeClass('hide').parent().siblings().find('.menu-body').addClass('hide'); }) }) </script> """ return v
a. 以后调用
#自定义的中间件加入到settings 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.middleware.md.RbacMiddleware', ]
#后端 from rbac import service 1. 用户登录后,拿到用户的ID,调用permission_session()函数(函数代码还没写) 函数获取用户角色的权限,格式如: “”“ user_permission_dict = { '/ah-index.html': ["GET","POST","DEL","Edit"], '/order.html': ["GET","POST","DEL","Edit"], '/index-(\d+).html': ["GET","POST","DEL","Edit"], } ”“” def login(): permission_session(用户ID,request) return ..... 2.setting中加入中间件,如上 3.#获取菜单 current_url= request.pathinfo menu_list = service.menu(用户ID,current_url) 4.尽量用simple_tag css = servicr.css() js = servicr.js() 5.前端 {{ css|safe }} {{ menu_list|safe }} {{ js|safe }}
代码下载地址:https://files.cnblogs.com/files/luchuangao/rbac.zip
生产环境使用
rbac组件完整版下载地址:https://files.cnblogs.com/files/luchuangao/rbac%E5%AE%8C%E6%95%B4%E7%89%88.zip
a. RBAC原理:
原理: 1. 简单管理 角色单一,无需使用权限管理 2. 角色多管理(权限) a. 登录 session放置用户信息(检测是否已经登录) session放置权限信息(检测是否有权访问) { '/index.html':[GET,EDIT], '/order.html':[GET,EDIT], '/xxx.html':[GET,EDIT...], '/xxx.html':[GET,EDIT...], '/xxx.html':[GET,EDIT...], '/xxx.html':[GET,EDIT...], '/xxx.html':[GET,EDIT...], '/xxx.html':[GET,EDIT...], '/xxx.html':[GET,EDIT...], '/xxx.html':[GET,EDIT...], '/xxx.html':[GET,EDIT...], '/xxx.html':[GET,EDIT...], '/xxx.html':[GET,EDIT...], } session放置菜单权限信息(用于生成动态多级菜单) b. 访问网站其他功能: http://www.baiuc.om/xxx.hmtl - 获取当前访问的URL, request.path_info - /xxx.hmtl?md=get 匹配1 /xxx.hmtl session放置权限信息(检测是否有权访问) { '/index.html':[GET,EDIT], '/order.html':[GET,EDIT], '/xxx.html':[GET,EDIT...], } 匹配2 /xxx.hmtl session放置权限信息(检测是否有权访问) { '/index.html':[GET,EDIT], '/order.html':[GET,EDIT], '/xxx.html':[GET,EDIT...], } request.permission_code = "EDIT" request.permission_code_list = [GET,EDIT...] PS: 中间件实现 c. 视图函数 def xxx(request): request.permission_code = "EDIT" # 业务逻辑的编写 request.permission_code_list = [GET,EDIT...] # 前端显示功能按钮 d. 模板 e. 创建动态菜单【多级菜单】 session中获取菜单权限信息(用于生成动态多级菜单) - 当前用户权限 - 所有菜单 1. 权限挂到菜单上 2. 菜单父子关系处理 3. 递归生成菜单 辅助: css js 推荐:simple_tag 使用:【详细见README】 1. 导入rbac - 表 - 中间件 - service - simple_tag 2. 注册app 3. 用户登录 初始化权限信息: service.initail_permission(request,user_id) 4. settings中配置中间件 5. {% rbac_menu reqeust %} 其他配置: ...
b. 应用实例
具体功能: 临时工: 报障单 trouble.html 查看列表 look 查看详细 detail 普通员工: 报障单 trouble.html 创建 post 删除 del 修改 edit 查看列表 look 查看详细 detail 运维人员: 报账单 解决报障单 trouble-kill.html 总监: 报障单 解决报障单 报表 report.html
报障系统示例代码:https://files.cnblogs.com/files/luchuangao/rbacdemo.zip
制作报表参考:制作报表工具