深度权限管理系统
需求:
最终保证,每个人有不同的权限,而权限的分配主要是通过url来判定
先来看models表的演变过程,以及需求的数据演变过程
2 张表
用户表 权限表 一对多关系
joker /user/
/user/add ...
那如果这样,新来名员工就会配置N个权限,操作复杂?如果加入角色?
3 张表 外加 1张 多对多表 用户表 多对多 角色 一对多 权限表 joker 经理 /user/... ceo /all/...

permiss_list=user.roles.filter(permissions__url__isnull=False).value('permissions__title','permissions__url').distinct() # request.session['is_login'] = True request.session['user'] = user.username # print(user.username) url_list = [] for url in permiss_list: print(url) url_list.append(url['permissions__url']) request.session[settings.PERMISSION_SESSION_KEY] = url_list 添加到配置文件中去,在判断用户每次的输入url是否在这里
如果这样,新来的员工直接分配角色就可以了,对?没错这样基本实现了权限分配,但是对于我们开发前端的话,我们必须直到用户进来有多少权限,才能针对页面做按钮级别的控制,因为不能用户进来所有的增删改查按钮都在?那如果加入权限组?
4 张表 外加 2 张多对多表 用户表 多对多 角色 一对多 权限表(增加code字段,对url的解释,例如/user/,code为list) 多对一 权限组 joker 经理 /user/... 1 ceo /all/.... 1 注意去重

想要得到的数据效果,该用户下的权限组ID下的所有权限 1 {'urls': ['/userinfo/', '/userinfo/del/(\\d+)', '/userinfo/edit/(\\d+)', '/userinfo/add/'], 'codes': ['list', 'del', 'edit', 'add']} 2 {'urls': ['/order/', '/order/del/(\\d+)', '/order/add/', '/order/edit/(\\d+)'], 'codes': ['list', 'del', 'add', 'edit']} permiss_list = user.roles.all().values('permissions__code','permissions__group_id','permissions__url').distinct() # permiss_dict = {} for item in permiss_list: codes = item['permissions__code'] group_id = item['permissions__group_id'] urls = item['permissions__url'] if permiss_dict.get(group_id): permiss_dict[group_id]['codes'].append(codes) permiss_dict[group_id]['urls'].append(urls) else: permiss_dict[group_id]={ 'codes':[codes,], 'urls':[urls,] } for k,v in permiss_dict.items(): print(k,v) request.session['is_login'] = True request.session['user'] = user.username # user 传过来的是一个对象 request.session[settings.PERMISSION_SESSION_KEY] =permiss_dict 将权限列表加入到settings里面去,在根据用户输入的url来判断有什么权限
这样完全实现了权限分配,那我们将这个权限控制放在中间键吧?这样可以每次只要请求过来就可以进行判定,很方便

from django.utils.deprecation import MiddlewareMixin import re from django.shortcuts import HttpResponse from django.conf import settings class RbacMiddleware(MiddlewareMixin): def process_request(self, request): # 1. 获取白名单,让白名单中的所有url和当前访问url匹配 for reg in settings.PERMISSION_VALID_URL: if re.match(reg, request.path_info): return None # 2. 获取权限 premiss_list = request.session.get(settings.PERMISSION_SESSION_KEY) if not url_list: return HttpResponse('未获取到当前用户的权限信息,无法访问') # 3. 对用户请求的url进行匹配 flag = False for reg in url_list: # 需要注意 regx = "^%s$" % (reg,) if re.match(regx, request.path_info): print('====') flag = True # break return None if not flag: return HttpResponse('无权访问')
我们看到中间键里面有一些针对settings的配置我们看下配置了哪些?
1. settings导入方法是 from django.conf import settings 2. 中间键的注册 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' 应用.目录.目录.类名 过滤url验证 ] 3. 记得在数据机构化的时候,我们把数据加到了settings里面,所以要在里面加入 PERMISSION_SESSION_KEY = "url_listt" # 权限字典,包含列表
MENU_SESSION_KEY = 'menu_list' # 后面的菜单设计
4. 中间键过滤的url,我们发现一个问题,如果登陆都不再权限内,那将会无限死循环,所以我们需要一个url白名单 PERMISSION_VALID_URL = [ # 白名单 '/login/', '/admin/.*', '/index/', '/menu/', ] 这里面的url都是可以不用进行url验证
当我们url验证通过,拿到用户下的权限组下的所有权限,那我们如何处理这些权限呢,从而实现在前端可以针对性的控制按钮?
还记得我们code么,url的别名?我们只需要判断用户输入的url是否有通过验证,通过就会有对应的code?

class PermissCode: def __init__(self, premiss_codes_list): self.premiss_codes_list = premiss_codes_list def has_add(self): if 'add' in self.premiss_codes_list: return True def has_del(self): if 'del' in self.premiss_codes_list: return True def has_edit(self): if 'edit' in self.premiss_codes_list: return True def orderinfo(request): premiss_codes_list = request.premiss_codes_list # 拿到 code列表 page_permission = PermissCode(premiss_codes_list) # 实例化 return render(request,'order.html',{'page_permission':page_permission}) data_list是数据,只需要观察如何显示权限的就可以了 {% block content %} {% if page_permission.has_add %} <a href="/userinfo/add/">添加</a> {% endif %} <table> {% for row in data_list %} <tr> <td>{{ row.id }}</td> <td>{{ row.name }}</td> {% if page_permission.has_edit %} <td><a href="#">编辑</a></td> {% endif %} {% if page_permission.has_del %} <td><a href="#">删除</a></td> {% endif %} </tr> {% endfor %} </table> {% endblock %}
html用到了模版,因为页面都一样,这样可以更加的弹性,伸缩
上面数据展示的代码有rbac应用,server目录下的init_permission初始化获取,视图里面调用这个函数就可以
加入菜单管理
菜单设计顾名思义,就是当我选中一个左侧菜单时候,显示该菜单下的权限,其他菜单下的权限要闭合?
那我们肯定是要增加表,那么菜单表要跟谁对应呢,应该是权限组,你要想,我们上面数据最终得到的是权限组=[url:{},code:{}],那我们将菜单组跟权限组关联,是不是就可以替代权限组的位置,而且在前端显示的时候,菜单下对应的权限也会多,也很美观。
5 张表 外加 2 张多对多表 用户表 多对多 角色 一对多 权限表(增加code字段,对url的解释,例如/user/,code为list) 多对一 权限组 多对一 菜单 joker 经理 /user/... 1 菜单一 1 ceo /all/... 1 菜单二 1 注意去重
我们首先从数据库中拿到这样的数据放入到settings中,我们有专门的函数来处理。
# user 是个对象
menu_list = user.roles.all().values('permissions__id', # 权限ID 'permissions__url', # 权限 url 'permissions__code', # 权限 code 别名 'permissions__title', # 权限名称 'permissions__is_menu', # 是否是菜单 'permissions__group_id', # 权限组ID 'permissions__parent_id', # 自关联ID 'permissions__group__menu__id', # 菜单ID 'permissions__group__menu__title').distinct() # 菜单名称 print(menu_list) # 用于生成菜单 menu_list_all = []
for item in menu_list: tpl = { 'id': item['permissions__id'], 'title': item['permissions__title'], 'url': item['permissions__url'], 'menu_gp_id': item['permissions__parent_id'], 'menu_id': item['permissions__group__menu__id'], 'menu_title': item['permissions__group__menu__title'], } menu_list_all.append(tpl) request.session[settings.MENU_SESSION_KEY] = menu_list_all ######### 菜单 print(menu_list_all)
{'title': '用户列表', 'url': '/userinfo/', 'pid': None, 'id': 1, 'menu_id': 1, 'menu_title': '菜单一'},
{'title': '用户删除', 'url': '/userinfo/del/(\\d+)', 'pid': None, 'id': 2, 'menu_id': 1, 'menu_title': '菜单一'},
{'title': '订单列表', 'url': '/order/', 'pid': None, 'id': 3, 'menu_id': 2, 'menu_title': '菜单二'},
{'title': '订单删除', 'url': '/order/del/(\\d+)', 'pid': None, 'id': 4, 'menu_id': 2, 'menu_title': '菜单二'},
{'title': '用户修改', 'url': '/userinfo/edit/(\\d+)', 'pid': None, 'id': 5, 'menu_id': 1, 'menu_title': '菜单一'},
{'title': '用户添加', 'url': '/userinfo/add/', 'pid': None, 'id': 6, 'menu_id': 1, 'menu_title': '菜单一'},
{'title': '订单添加', 'url': '/order/add/', 'pid': None, 'id': 7, 'menu_id': 2, 'menu_title': '菜单二'},
{'title': '订单修改', 'url': '/order/edit/(\\d+)', 'pid': None, 'id': 8, 'menu_id': 2, 'menu_title': '菜单二'}
我们在将上面得到数据进行数据结构化?
菜单一:{ 1: { 'active': True, # 是否展开
'menu_id': 1, 'children': [{'url': '/userinfo/', 'active': True, 'title': '用户列表'}, {'url': '/order/', 'active': None, 'title': '订单列表'}], 'menu_title': '菜单一'} } } '''
如果得到上面的结构,我们就可以通过active的值来判定谁展开,谁关闭。children代表了用户权限,通过active来判定是否选中,是通过用户的url来判定。
并且通过孩子的active属性来判定父的active属性值,默认为None,但是当孩子的active为True,父就会随之变为True,展开。
我们知道为何要这种数据结构了,还有个问题?如果视图非常多(也肯定非常多,比如用户列表,订单列表,还要增删改查),我们不能在视图里面重复过多的写入这种数据结构的演变,我们可以写入一个单独文件内,这样去调用就可以了,还寂寞模版么?所有的html都是基于它展现出来,而结构出来的菜单设计也是需要跟模版进行渲染,所以,我们把结构化数据,和一部分模版上的左边菜单进行渲染,利用到了register.inclusion_tag方法?
应用下面创建 templatetags目录,在创建rbac.py文件

from django.template import Library register = Library() # 实例化 @register.simple_tag def menu_html(): return '菜单' # 返回什么就是什么 from django.conf import settings import re @register.inclusion_tag('xxxxx.html') def menu_html_new(request): menu_list = request.session.get(settings.MENU_SESSION_KEY) current_url = request.path_info # 请求的URL menu_dict = {} for item in menu_list: if not item['menu_gp_id']: menu_dict[item['id']] = item print(menu_dict) ''' {1: {'id': 1, 'menu_id': 1, 'url': '/userinfo/', 'menu_title': '菜单一', 'menu_gp_id': None, 'title': '用户列表'}, 3: {'id': 3, 'menu_id': 1, 'url': '/order/', 'menu_title': '菜单一', 'menu_gp_id': None, 'title': '订单列表'}} ''' for item in menu_list: regex = "^{0}$".format(item['url']) if re.match(regex, current_url): menu_gp_id = item['menu_gp_id'] if menu_gp_id: menu_dict[menu_gp_id]['active'] = True else: menu_dict[item['id']]['active'] = True print('=======') print(menu_dict) ''' { { 1: {'active': True, 'id': 1, 'menu_id': 1, 'url': '/userinfo/', 'menu_title': '菜单一', 'menu_gp_id': None, 'title': '用户列表'}, # 默认展开 3: {'id': 3, 'menu_id': 1, 'url': '/order/', 'menu_title': '菜单一', 'menu_gp_id': None, 'title': '订单列表'}} # 不展开 } ''' result = {} for item in menu_dict.values(): active = item.get('active') menu_id = item['menu_id'] if menu_id in result: result[menu_id]['children'].append({'title': item['title'], 'url': item['url'], 'active': active}) if active: result[menu_id]['active'] = True else: result[menu_id] = { 'menu_id': item['menu_id'], 'menu_title': item['menu_title'], 'active': active, 'children': [ {'title': item['title'], 'url': item['url'], 'active': active} ] } print(result) ''' ## 菜单 菜单一:{ 1: { 'active': True, 'menu_id': 1, 'children': [{'url': '/userinfo/', 'active': True, 'title': '用户列表'}, {'url': '/order/', 'active': None, 'title': '订单列表'}], 'menu_title': '菜单一'} } } ''' return {'menu_dict':result}
里面的xxxx.html是判定左边菜单的展示还是收缩,还判定权限是否被选中

{% for value in menu_dict.values %} <div class="item-title"> <div class="header item">{{ value.menu_title }}</div> {% if value.active %} <div class="body"> {% else %} <div class="body hide"> {% endif %} {% for child in value.children %} {% if child.active %} <a href="{{ child.url }}" class="active">{{ child.title }}</a> {% else %} <a href="{{ child.url }}">{{ child.title }}</a> {% endif %} {% endfor %} </div> </div> {% endfor %}
将上面结构数据,和html渲染过后的数据放回模版里面 dynamic_menu.html

{% load rbac %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <script src="https://cdn.bootcss.com/jquery/3.1.1/jquery.js"></script> <link rel="stylesheet" href="/static/rbac/rbac.css"> </head> <body> <div class="pg-header">表头</div> <div class="context"> <div style="width: 20%;float: left;background-color: cadetblue"> {# 这里代码移交至 #} {% menu_html_new request%} </div> <div style="width: 80%;float: left"> {% block content %} {% endblock %} <script src="/static/rbac/rbac.js"></script> </div> </div> </body> </html>
其他的html文件只需要继承就可以了,在block里面写入当前页面需要的内容
例如用户添加页面
{% extends "dynamic_menu.html" %} {% block content %} <h1>添加用户页面</h1> <input type="text"> {% endblock %}
最后你需要注意的是,settings文件配置了static静态文件,你可以将一些css,js的引用放到这里,收缩你的代码
项目加入权限系统的过程

1. rbac清空migrations目录(除__init__.py以外) 2. 业务的用户表和权限的用户表OneToOne关联,如: from django.db import models from rbac import models as rbac_model class DepartMent(models.Model): """ 部门 """ title = models.CharField(max_length=32) class User(models.Model): user_info = models.OneToOneField(to=rbac_model.UserInfo) nickname = models.CharField(max_length=32) momo = models.CharField(max_length=32) gender_choices = ( (1,'男'), (2,'女'), ) gender = models.IntegerField(choices=gender_choices) 3. 通过页面admin录入权限信息 4. 用户登录成功之后,初始化权限和菜单信息 init_permission(权限的用户表对象,request) 加入权限相关的配置: PERMISSION_SESSION_KEY = “url_list” MENU_SESSION_KEY = “menu_list” 5. 对用户请求的url进行权限的验证 应用中间件: MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', ... 'rbac.middlewares.rbac.RbacMiddleware', #中间键对于url的判定 ] 中间键里面有白名单, setting.py PERMISSION_VALID_URL = [ '/login/', '/admin/.*', ] 有针对数据权限的判定 PERMISSION_SESSION_KEY 只要通过验证,在视图函数的request中 permission_codes字段: [list,add,edit,del....] 如果想要面向对象方式, from rbac.permission.base import BasePermission class PermissCode: def __init__(self, premiss_codes_list): self.premiss_codes_list = premiss_codes_list def has_add(self): if 'add' in self.premiss_codes_list: return True 给前端传过实例之后,就可以判断有没有这个属性 判断url所有权限,通过code判定,然后可以针对按钮级别是否显示 6. 动态菜单 在Html模板中引入: {% load rbac %} 引入样式 <link rel='stylesheet' href='/static/rbac/rbac.css' /> <script src='/static/rbac/rbac.js' /> {% menu request %} 推荐:放到母板中。