菜单权限管理终章


深度权限管理系统

需求:

最终保证,每个人有不同的权限,而权限的分配主要是通过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('无权访问')
中间键过滤url

我们看到中间键里面有一些针对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 %}
xxxx.html

将上面结构数据,和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 %}

   推荐:放到母板中。
权限与项目的结合


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM