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
View Code

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'文件渲染成我们想要的菜单。我们需要这样的菜单时,只需要在页面按照规则引入此方法,便可得到我们想要的菜单,这样是不是很灵活?具体使用如下例:

五、组件调用说明

 

posted @ 2018-01-23 22:28  骑猪走秀  阅读(103)  评论(0)    收藏  举报