python之路_day104_django 权限组件项目文档
权限系统实现方式:
1、目标就是实现一个权限的组件,以后再写任何系统的时候可以直接进行使用;
2、用户登录成功后,将此用户的权限信息存放在session,用户再访问其他视图时,将访问的url和session中权限url进行匹配;
3、考虑每一个视图函数在被访问前都需要进行用户的权限验证,所以步骤2中的权限匹配放在中间件process_request函数中
ps:五种中间件介绍
- process_request - prccess_view - process_exception - process_response - process_template....
django中,中间件中的主要四个方法的执行顺序如所写,从上到下。按照settings中配置的中间件的顺序,从上到下执行process_request方法,此方法可有可无返回值,若有返回值,则按中间件中五种方法的执行顺序,原路返回,后面的中间件不再执行;然后按照中间件的顺序,从上到下执行prccess_view方法;再按照配置中间件的顺序从下到上执行process_exception方法,若有异常则抛出异常;最后按照中间件的顺序从下往上逐个执行process_response方法,此方法一定会有返回值。
一、表结构的设计
from django.db import models # Create your models here. class UserInfo(models.Model): ''' 用户信息表:与角色表建立多对关系 ''' name=models.CharField(max_length=32,verbose_name="用户名") pswd=models.CharField(max_length=64) email=models.CharField(max_length=64) roles=models.ManyToManyField(to="Role") def __str__(self): return self.name class Role(models.Model): ''' 角色表:与权限表创建多对多关系,此表主要是方便给同一类人批量增加权限 ''' name=models.CharField(max_length=32,verbose_name="角色名称") permissions=models.ManyToManyField(to="Permission") def __str__(self): return self.name class Menu(models.Model): ''' 菜单表 ''' title=models.CharField(max_length=32) def __str__(self): return self.title class PermissionGroup(models.Model): ''' 权限分组表:与菜单表建立多对一关系,主要解决可以作为菜单显示的权限列表太多情况,通过菜单 表,可以将权限菜单作为二级菜单存在菜单下面 ''' caption=models.CharField(max_length=16) menu=models.ForeignKey(to="Menu",default=1) def __str__(self): return self.caption class Permission(models.Model): ''' 权限表:与权限分组表建立多对一关系,将同一类权限归为一组,方便粒度到按钮级别的权限实现 通过parent字段建立自关联,主要用于自动生成菜单时候判断哪些可以作为菜单项显示的 -------(如编辑和删除权限显然不可以作为菜单显示) ''' title=models.CharField(max_length=32,verbose_name="权限名称") url=models.CharField(max_length=128) code=models.CharField(max_length=16,default="list") group=models.ForeignKey(to="PermissionGroup",default=1) parent=models.ForeignKey(to="Permission",null=True,related_name="permissions") def __str__(self): return self.title
二、权限验证实现
1、获取权限
用户登录成功后,根据当前登录用户对象跨表查询其所属的权限信息。将查询结果按照权限组的不同,组织成如下实例数据,字典中key为所属组的id,value包含属于此组权限的url及其对应的codes,并将此数据存入session。
生成数据代码''' { 1: { 'urls': ['/users/', '/users/add/', '/users/edit/(\\d+)/', '/users/del/(\\d+)/'], 'codes': ['list', 'add', 'edit', 'del'] }, 2: { 'urls': ['/orders/', '/orders/add/', '/orders/edit/(\\d+)/', '/orders/del/(\\d+)/'], 'codes': ['list', 'add', 'edit', 'del'] } } '''
2、匹配权限
如上用户登录以后,会将其所属的权限信息存入session,当用户再访问其他权限url时候,就需要将访问的url与session中所属的权限进行匹配,如果匹配成功,用户可以正常访问此url,若果session中没有包含当前访问的url的权限,则告知用户没有权限,拒绝访问。因为每个url对应的视图函数都需要做这样的权限匹配。所以将权限匹配逻辑写成中间件,这样每次访问任何函数,都会先走中间进行权限匹配,解决了复杂的代码冗余。
from django.conf import settings from django.utils.deprecation import MiddlewareMixin import re from django.shortcuts import HttpResponse class RbacMiddleware(MiddlewareMixin): def process_request(self,request): #1、获取白名单url,让白名单的url通过访问,不用进行权限验证 for reg in settings.PERMISSION_VALID_URLS: regx="^%s$" %reg if re.match(regx,request.path_info): return None #2、获取权限字典 permission_dict=request.session.get(settings.PERMISSION_SESSION_DICT) if not permission_dict: #用户没有登录情况(未设置session) return HttpResponse("未能获得用户权限信息,请确认是否登录") #3、给用户分配访问权限 flag=False for items in permission_dict.values(): for reg in items["urls"]: regx = "^%s$" % reg if re.match(regx,request.path_info): flag=True break if flag: request.permission_codes=items["codes"] break if not flag: return HttpResponse("没有权限哦!")
注意两点:(1)settings中必须配置url白名单,在中间件中会取到这样的白名单列表,从而进行循环判断。如果访问url包含在白名单列表中,则可以访问;(2)中间件文件必须记得在setting中进行配置。
三、粒度到按钮级别的权限实现
按照如上所述,在中间件中进行权限匹配时,若匹配成功,则将此权限所属权限组中的权限codes进行保存。如下方式,然后在相应的页面中通过判断是否要渲染相应的按钮,有此权限则渲染此按钮,如果没有这个权限则不渲染,如此便实现了粒度到按钮级别的权限。

html文件:
{% if "edit" in request.permission_codes %} <a href="/editbook/{{ book.nid }}" type="button"class="btn btn-success"> <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> 编辑
</a> {% endif %} {%if "del" in request.permission_codes %} <a href="/deletebook/{{ book.nid }}" type="button"class="btn btn-danger"> <span class="glyphicon glyphicon-remove"aria-hidden="true"></span> 删除
</a> {% endif %}
但是对于这种方式,当我们有很多页面需要判断渲染这样按钮时,牵扯修改代码等是会很麻烦,不是很灵活。所以我们又将如上的判断定义成一个类,在相应的视图中实例化此类对象后,只需要在前端代码对实例对象的相应方法判断即可。具体如下:
定义类:
class BasePermisson(object): def __init__(self,codes): self.codes=codes def list(self): if "list" in self.codes: return True def add(self): if "add" in self.codes: return True def edit(self): if "edit" in self.codes: return True def delete(self): if "del" in self.codes: return True
实例化:
def users(request): '''引入类,并实例化''' PermissonObj=BasePermisson(request.permission_codes) return render(request,"userslist.html",locals())
html文件:
{% if PermissonObj.edit %} <a href="/editbook/{{ book.nid }}" type="button" class="btn btn-success"> <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> 编辑 </a> {% endif %} {% if PermissonObj.delete %} <a href="/deletebook/{{ book.nid }}" type="button" class="btn btn-danger"> <span class="glyphicon glyphicon-remove"aria-hidden="true"></span> 删除 </a> {% endif %}
四、动态生成菜单功能实现
1、权限信息存储
登录成功以后,将此user对象的相关权限信息在session里,主要数据的存储形式如下,这里主要需要说明一下权限表Session中的parent字段的作用,它是可为空字段,且属于自关联的字段,目的是将判断哪些权限是可以作为菜单显示的(为空的权限可以作为菜单,不为空的权限的不可以,但是当访问的是此权限时,让他绑定的可以作为菜单项的选中)
''' [ {'id': 5, 'title': '用户列表', 'url': '/users/', 'pid': None, 'menu_id': 1, 'menu_title': '用户菜单'}, {'id': 6, 'title': '增加用户', 'url': '/users/add/', 'pid': 5, 'menu_id': 1, 'menu_title': '用户菜单'}, {'id': 7, 'title': '编辑用户', 'url': '/users/edit/(\\d+)/', 'pid': 5, 'menu_id': 1, 'menu_title': '用户菜单'}, {'id': 8, 'title': '删除用户', 'url': '/users/del/(\\d+)/', 'pid': 5, 'menu_id': 1, 'menu_title': '用户菜单'}, {'id': 9, 'title': '订单列表', 'url': '/orders/', 'pid': None, 'menu_id': 1, 'menu_title': '用户菜单'}, {'id': 10, 'title': '增加订单', 'url': '/orders/add/', 'pid': 9, 'menu_id': 1, 'menu_title': '用户菜单'}, {'id': 11, 'title': '编辑订单', 'url': '/orders/edit/(\\d+)/', 'pid': 9, 'menu_id': 1, 'menu_title': '用户菜单'}, {'id': 12, 'title': '删除订单', 'url': '/orders/del/(\\d+)/', 'pid': 9, 'menu_id': 1, 'menu_title': '用户菜单'} ] '''
menu_list=[] for item in permission_list: temp={"id":item["permissions__id"], "title":item["permissions__title"], "url":item["permissions__url"], "pid":item["permissions__parent_id"], "menu_id":item["permissions__group__menu_id"], "menu_title":item["permissions__group__menu__title"], } menu_list.append(temp) request.session[settings.MENU_SESSION_LIST]=menu_list
2、获取并解析数据
在视图函数中函数中获取上述储存数据并进行解析,整理成如下数据格式的数据:
''' menu_dict = { 1: { 'title': '菜单一', 'active': True, 'children': [ {'title': '权限一', 'url': '/xxxxx/', 'active': False}, {'title': '权限二', 'url': '/xxxxx/', 'active': True}, ] }, 2: { 'title': '菜单二', 'active': False, 'children': [ {'title': '权限三', 'url': '/xxxxx/', 'active': False}, {'title': '权限四', 'url': '/xxxxx/', 'active': False}, ] } } '''
组织上述数据的思路是以不同菜单名进行分类,将此属于此菜单的且可以作为菜单的权限信息加单对应的children列表中。具体实现代码如下,最终将menu_dict数据返给前端通过判断渲染相应的菜单,如果'active': True,菜单展开,对应的二级菜单中'active': True为当前访问的或者当前访问的是其自关联的儿子权限,应该让其变红;否则让其隐藏。
menu_list = request.session[settings.MENU_SESSION_LIST] current_url = request.path_info menu_dict_new = {} ''' 循环取出可以作为菜单的权限,组成初步的字典数据 ''' for item in menu_list: if not item["pid"]: item["active"] = False menu_dict_new[item["id"]] = item print(menu_dict_new) ''' 循环匹配当前访问对应的url,如果此权限可以是本身可以做标签的, 将给其增加active=True字段,如果当前url不可以做标签(pid不为空) 则给其关联的可以做菜单的权限修改active=True,上述已经获得数据。 ''' for item in menu_list: pid = item["pid"] regs = "^%s$" % item["url"] if re.match(regs, current_url): if pid: menu_dict_new[pid]["active"] = True else: item["active"] = True print(menu_dict_new) ''' 将同一个菜单下可以做菜单的权限组成最终想要的数据 ''' menu_dict = {} for item in menu_dict_new.values(): menu_id = item["menu_id"] if menu_id in menu_dict: temp = {"title": item['title'], "url": item['url'], "active": item["active"]} menu_dict[menu_id]["children"].append(temp) if item["active"]: menu_dict[menu_id]["active"] = True else: menu_dict[menu_id] = { 'title': item["menu_title"], 'active': item["active"], 'children': [{"title": item['title'], "url": item['url'], "active": item["active"]}] }
3、自定义模板语言方法
理论上述便已完成了自动渲染菜单的功能,但是对于组件来说还不是很灵活,每一个需要渲染菜单的视图都要按照如上组织数据,然后在前端通过判断数据渲染菜单,这样不是很灵活,如果菜单能通过在页面调用模板语言的方法自动实现渲染,那就会变得灵活很多。这就是我们要自定义模板方法inlusion_tag需要解决的。实现规则:
规则:
a. 在任意已经注册的app中,创建一个 templatetags 的目录
b. 创建任意 py文件
c. 创建一个Libiary的对象,且对象的名称叫register
具体代码如下:
import re from django.conf import settings from django.template import Library register=Library() @register.simple_tag def func(a1,a2): ''' simple_tag: 通过在页面引用的时候,会将函数的返回值直接在页面进行显示 :param a1: :param a2: :return: ''' return a1+a2 @register.inclusion_tag('rbac/rbac_menu.html') def menu(request): ''' inclusion_tag: 函数的返回值在指定的'rbac/rbac_menu.html'文件中进行渲染,通过在页面引用时会将渲染的整体效果在页面显示 :param request: :return: ''' menu_list = request.session[settings.MENU_SESSION_LIST] current_url = request.path_info menu_dict_new = {} for item in menu_list: if not item["pid"]: item["active"] = False menu_dict_new[item["id"]] = item print(menu_dict_new) for item in menu_list: pid = item["pid"] regs = "^%s$" % item["url"] if re.match(regs, current_url): if pid: menu_dict_new[pid]["active"] = True else: item["active"] = True print(menu_dict_new) menu_dict = {} for item in menu_dict_new.values(): menu_id = item["menu_id"] if menu_id in menu_dict: temp = {"title": item['title'], "url": item['url'], "active": item["active"]} menu_dict[menu_id]["children"].append(temp) if item["active"]: menu_dict[menu_id]["active"] = True else: menu_dict[menu_id] = { 'title': item["menu_title"], 'active': item["active"], 'children': [{"title": item['title'], "url": item['url'], "active": item["active"]}] } print(menu_dict) return {"menu_dict":menu_dict}
rbac_menu.html代码:
<div class="col-sm-2" style="background-color: #cccccc;height: 710px"> <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true"> {% for menu in menu_dict.values %} <div class="panel panel-default"> <div class="panel-heading" role="tab" id="headingOne"> <h4 class="panel-title"> <a role="button" data-toggle="collapse" data-parent="#accordion" href="/booklist/" aria-expanded="true" aria-controls="collapseOne"> {{ menu.title }} </a> </h4> </div> {% if menu.active %} <div class="panel-body"> {% else %} <div class="panel-body hide"> {% endif %} {% for child in menu.children %} {% if child.active %} <p><a style="padding-left: 20px;color: red" class="active">{{ child.title }}</a></p> {% else %} <p><a style="padding-left: 20px">{{ child.title }}</a></p> {% endif %} {% endfor %} </div> </div> {% endfor %} </div> </div>
@register.inclusion_tag('rbac/rbac_menu.html')会将装饰的函数的返回值给到'rbac/rbac_menu.html'文件渲染成我们想要的菜单。我们需要这样的菜单时,只需要在页面按照规则引入此方法,便可得到我们想要的菜单,这样是不是很灵活?具体使用如下例:

五、组件调用说明


浙公网安备 33010602011771号