权限组件

编辑本文章

wupeiqi博客

权限控制

创建表格

from django.db import models


# Create your models here.

class Permission(models.Model):
    """
    权限表
    """
    title=models.CharField(verbose_name='标题',max_length=32)
    url=models.CharField(verbose_name='含正则的URL',max_length=256)
    def __str__(self):
        return self.title

class Role(models.Model):
    """
    角色表
    """
    title = models.CharField(verbose_name="角色名称", max_length=32)
    permissions = models.ManyToManyField(verbose_name="拥有的所有权限", to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户信息表
    """
    name = models.CharField(verbose_name="用户名", max_length=32)
    password = models.CharField(verbose_name="密码", max_length=64)
    email = models.CharField(verbose_name="邮箱", max_length=32)
    roles = models.ManyToManyField(verbose_name="所拥有的角色", to="Role", blank=True)

    def __str__(self):
        return self.name
View Code

权限控制步骤

  1. 登录页面是否有访问权限
  2. POST请求,用户登录验证是否合法
  3. 获取当前用户相关所有权限,并放入session
  4. 再次请求,后端编写中间件对用户访问的url进行权限判断(是否在session中)

在业务系统中创建登录页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login/" method="post">
    {% csrf_token %}
    <input type="text" name="user">
    <input type="text" name="pwd">
    <input type="submit" value="提交"><span style="color:red;">{{ msg }}</span>
</form>
</body>
</html>
View Code

在业务系统中实现登录逻辑

登录视图模块

from django.shortcuts import HttpResponse,render,render_to_response,redirect
from django.contrib import auth
from rbac import models
def login(request):
    if request.method=='GET':
        return render(request,'login.html')
    else:
        user=request.POST.get('user')
        pwd=request.POST.get('pwd')
        current_user=models.UserInfo.objects.filter(name=user,password=pwd).first()
        if not current_user:
            return render(request,'login.html',{'msg':'用户名或密码错误'})
        #根据当前用户信息获取该用户所有权限,并放入session
        #role_list=current_user.roles.all()
        #permissions__isnull=False过滤没有分配权限的角色
        permission_queryset=current_user.roles.filter(permissions__isnull=False).values("permissions__id","permissions__url").distinct()
        permission_list=[]
        for item in permission_queryset:
            permission_list.append(item['permissions__url'])
        request.session['permission_url_list_key']=permission_list
        return redirect('/customer/list/')
View Code

登录成功后将跳转到指定页面,同步更新数据库中的session

通过中间件验证

在业务系统中创建中间件模块

在业务系统中添加中间件组件

简易版中间件实现登录验证

from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
import re
class CheckPermission(MiddlewareMixin):
    """
    用户权限信息校验
    """
    def process_request(self,request):
        """
        当用户请求进入时,触发
        1.获取当前请求url
        2.在session数据库中获取当前用户权限列表
        3.匹配url列表,即匹配权限
        :param request:
        :return:
        """
        #白名单URL,无需登录验证都可访问
        valid_url_list=[
            '/login/',
            '/admin/.*'
        ]
        current_url=request.path_info
        for valid_url in valid_url_list:
            reg='^%s$' % valid_url
            if re.match(reg,current_url):
                return None#在白名单中,无需进行验证,直接返回None

        permission_list=request.session.get('permission_url_list_key')#获取当前用户的权限列表
        if not permission_list:
            return HttpResponse('未获取到用户权限信息,请登录')
        #通过正则匹配url,需要开始于结束符号
        flag=False
        for url in permission_list:
            reg='^%s$' % url
            if re.match(reg,current_url):
                flag=True#拥有权限访问,不做任何处理
                break
        if not flag:
            return HttpResponse('无权访问')
View Code

完善权限控制,将rbac插件和业务系统拆分

1、用户登录与权限初始化拆分

  • 在rbac模块中创建service模块,创建init_permission.py文件
    def init_permission(current_user,request):
        """
        2、权限初始化
        根据当前用户信息获取该用户所有权限,并放入session
        role_list=current_user.roles.all()
        permissions__isnull=False过滤没有分配权限的角色
        :param current_user: 当前登录用户
        :param request: 请求的request
        :return:
        """
        permission_queryset = current_user.roles.filter(permissions__isnull=False).values("permissions__id",
                                                                                          "permissions__url").distinct()
        permission_list = []
        for item in permission_queryset:
            permission_list.append(item['permissions__url'])
        request.session['permission_url_list_key'] = permission_list
    View Code
  • 在web中应用init_permission模块
    from django.shortcuts import render,redirect
    from rbac.service.inti_permission import init_permission
    from rbac import models
    def login(request):
        if request.method=='GET':
            return render(request,'login.html')
        else:
            #1、用户登录
            user=request.POST.get('user')
            pwd=request.POST.get('pwd')
            current_user=models.UserInfo.objects.filter(name=user,password=pwd).first()
            if not current_user:
                return render(request,'login.html',{'msg':'用户名或密码错误'})
            #2、权限初始化
            init_permission(current_user,request)
            return redirect('/customer/list/')
    View Code

2、整改init_permission()方法,通过配置文件处理session的key问题

  • 在配置文件中添加关键字参数配置
    ########权限相关配置########
    PERMISSION_SESSION_KEY='permission_url_list_key'
  • 在项目和rbac中间件中引入配置
    from django.conf import settings

3、将权限相关的写入rbac组件中去,以便以后组件的应用

  • 将中间件写入rbac组件中

  • 配置文件中重写新的中间件
    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.rbac.RbacMiddleware',
    ]
    View Code
  • 将中间件里面的白名单写入setting文件中去
    ########rbac权限相关配置########
    PERMISSION_SESSION_KEY='permission_url_list_key'
    VALID_URL_LIST=[
                '/login/',
                '/admin/.*'
            ]
    View Code

动态菜单

一级菜单

初级菜单实现

1、表结构修改,添加菜单标记和菜单icon,并添加数据

class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)
    icon=models.CharField(verbose_name='菜单图标',max_length=32,null=True,blank=True)
    is_menu=models.BooleanField(verbose_name='是否可以做菜单',default=False)
View Code

2、用户登录时获取菜单信息,并保存到session

def init_permission(current_user,request):
    """
    2、权限初始化
    根据当前用户信息获取该用户所有权限信息和菜单信息,并放入session
    role_list=current_user.roles.all()
    permissions__isnull=False过滤没有分配权限的角色
    :param current_user: 当前登录用户
    :param request: 请求的request
    :return:
    """
    permission_queryset = current_user.roles.filter(permissions__isnull=False).values("permissions__id",                                                          "permissions__title",                                                  "permissions__is_menu",                                                 "permissions__iocn",                                                       "permissions__url",                                                       ).distinct()
    permission_list = []
    menu_list=[]
    for item in permission_queryset:
        permission_list.append(item['permissions__url'])
        if item['permissions__is_menu']:
            temp={'title':item['permissions__title'],
                 'icon':item['permissions__iocn'],
                 'url':item['permissions__url']}
            menu_list.append(temp)
    request.session[settings.PERMISSION_SESSION_KEY] = permission_list
    request.session[settings.MENU_SESSION_KEY] = permission_list
View Code

3、用户登录时在模板中显示菜单信息

<div class="static-menu">
    {% for item in request.session.menu_session_key %}
        <a href="{{ item.url }}" class="active">
            <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}</a>
    {% endfor %}
</div>    
View Code

通过inclusion来实现菜单展示

1、rbac中创建templatetags目录,并创建rbac.py模块

from django.template import Library
from django.conf import settings
register=Library()
@register.inclusion_tag('rbac/static_menu.html')
def static_menu(request):
    """
    创建一级菜单
    :return:
    """
    menu_list=request.session[settings.MENU_SESSION_KEY]
    return {"menu_list":menu_list}
View Code

2、在项目html文件中load相应tags

{% load rbac %}

3、在rbac下创建templates文件夹,并在其下创建rbac文件夹,之后新建static_menu.html模板

<div class="static-menu">
    {% for item in menu_list %}
        <a href="{{ item.url }}" class="active">
            <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}
        </a>
    {% endfor %}
</div>
View Code

4、在inclusion判断当前url是否为请求的url,实现菜单样式的区别,在template中判断两个变量是否相等用ifequal,非if

<div class="static-menu">
    {% for item in menu_list %}
        {% ifequal item.url request.path_info %}
            <a href="{{ item.url }}" class="active">
                <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}
            </a>
            {% else %}
            <a href="{{ item.url }}">
                <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}
            </a>
        {% endifequal %}
    {% endfor %}
</div>
View Code

在tag中将request传给模板

from django.template import Library
from django.conf import settings
register=Library()
@register.inclusion_tag('rbac/static_menu.html')
def static_menu(request):
    """
    创建一级菜单
    :return:
    """
    menu_list=request.session[settings.MENU_SESSION_KEY]
    return {"menu_list":menu_list,'request':request}
View Code

二级菜单

1、一级菜单无需url,不需要跳转,单独创建表

2、二级菜单从权限表中获得

3、session中存储的菜单信息结构

{
    1:{
        title:'信息管理',
        icon:'xxx',
        children:[
            {'title':'客户列表','url':'/xx/xx/'},
            {'title':'账单列表','url':'/payment/xx/'},
        ]
    },
    2:{
        title:'用户信息',
        icon:'xxx',
        children:[
            {'title':'客户列表','url':'/xx/xx/'},
            {'title':'账单列表','url':'/payment/xx/'},
        ]
    },
}
View Code

开发二级菜单

1、创建菜单表,存储一级菜单,并关联二级菜单

class Menu(models.Model):
    """
    一级菜单表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    icon = models.CharField(verbose_name='菜单图标', max_length=32, null=True, blank=True)

class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)
    # icon=models.CharField(verbose_name='菜单图标',max_length=32,null=True,blank=True)
    # is_menu=models.BooleanField(verbose_name='是否可以做菜单',default=False)
    menu=models.ForeignKey(verbose_name='所属菜单',to='Menu',null=True,blank=True,help_text='null表示非菜单,非null表示二级菜单',on_delete='CASCADE')
    def __str__(self):
        return self.title
View Code

 2、创建二级菜单inclusion的模板

<div class="multi-menu">
    {% for item in menu_dict.values %}
        <div class="item">
            <div class="title">
                <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span>{{ item.title }}
            </div>
            <div class="body">
                {% for per in item.children %}
                    <a href="{{ per.url }}" class="">{{ per.title }}</a>
                {% endfor %}
            </div>
        </div>
    {% endfor %}
</div>
View Code

  3、重写inclusion标签

@register.inclusion_tag('rbac/multi_menu.html')
def multi_menu(request):
    """
    创建二级菜单
    :return:
    """
    menu_dict=request.session[settings.MENU_SESSION_KEY]
    return {"menu_dict":menu_dict}
View Code

 4、创建rbac相关静态文件rbac.js和rbac.css

 rbac.js

$(function(jq){
    jq('.multi-menu .title').click(function () {
        $(this).next().toggleClass('hide');
    });
});
View Code

 rbac.css

.left-menu .menu-body .static-menu {
}

.left-menu .menu-body .static-menu .icon-wrap {
    width: 20px;
    display: inline-block;
    text-align: center;
}

.left-menu .menu-body .static-menu a {
    text-decoration: none;
    padding: 8px 15px;
    border-bottom: 1px solid #ccc;
    color: #333;
    display: block;
    background: #efefef;
    background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #efefef), color-stop(1, #fafafa));
    background: -ms-linear-gradient(bottom, #efefef, #fafafa);
    background: -moz-linear-gradient(center bottom, #efefef 0%, #fafafa 100%);
    background: -o-linear-gradient(bottom, #efefef, #fafafa);
    filter: progid:dximagetransform.microsoft.gradient(startColorStr='#e3e3e3', EndColorStr='#ffffff');
    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#fafafa',EndColorStr='#efefef')";
    box-shadow: inset 0px 1px 1px white;
}

.left-menu .menu-body .static-menu a:hover {
    color: #2F72AB;
    border-left: 2px solid #2F72AB;
}

.left-menu .menu-body .static-menu a.active {
    color: #2F72AB;
    border-left: 2px solid #2F72AB;
}

.multi-menu .item {
    background-color: white;
}

.multi-menu .item > .title {
    padding: 10px 5px;
    border-bottom: 1px solid #dddddd;
    cursor: pointer;
    color: #333;
    display: block;
    background: #efefef;
    background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #efefef), color-stop(1, #fafafa));
    background: -ms-linear-gradient(bottom, #efefef, #fafafa);
    background: -o-linear-gradient(bottom, #efefef, #fafafa);
    filter: progid:dximagetransform.microsoft.gradient(startColorStr='#e3e3e3', EndColorStr='#ffffff');
    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#fafafa',EndColorStr='#efefef')";
    box-shadow: inset 0 1px 1px white;
}

.multi-menu .item > .body {
    border-bottom: 1px solid #dddddd;
}

.multi-menu .item > .body a {
    display: block;
    padding: 5px 20px;
    text-decoration: none;
    border-left: 2px solid transparent;
    font-size: 13px;
}

.multi-menu .item > .body a:hover {
    border-left: 2px solid #2F72AB;
}

.multi-menu .item > .body .active {
    border-left: 2px solid #2F72AB;
}
View Code

 中间件验证用户权限

from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse,redirect
import re
class CheckPermission(MiddlewareMixin):
    """
    用户权限信息校验
    """
    def process_request(self,request):
        """
        用户请求进入时触发该方法
        :param request:
        :return:
        """
        """
        1.获取当前用户请求的URL
        2.获取当前用户在session中保存的url权限列表
        3.匹配URL,判断用户请求的url是否在权限列表url中
        """
        valid_url_list=['/login/','/admin/*']
        current_url=request.path_info
        #白名单验证
        for valid_url in valid_url_list:
            if re.match(valid_url,current_url):
                return None#跳过下面代码,即中间件不再拦截
        #访问URL不在白名单,继续验证
        permission_url_list=request.session.get('permission_url_list','')
        print(current_url)
        print(permission_url_list)
        if not permission_url_list:
            #为获取到用户权限信息,直接跳转到login页面
            return redirect('/login/')
        flag=False
        for url in permission_url_list:
            reg="^%s$" % url
            if re.match(reg,current_url):
                flag=True
                break
        if not flag:
            return HttpResponse("无权访问")
View Code

 5、在项目layout文件中引入css和js,注意js应用在jQuery下方

<link rel="stylesheet" href="{% static 'rbac/css/rbac.css' %} "/>
<script src="{% static 'js/jquery-3.3.1.min.js' %} "></script>
<script src="{% static 'rbac/js/rbac.js' %} "></script>
View Code

PS:访问的时候可能找不到对应的静态文件,在setting文件中添加

STATIC_ROOT=os.path.join(BASE_DIR,'static')

根据当前访问URL确定是否展开和激活二级菜单

知识点:

  1. 对字典进行排序,字典排序是对key排序,对原字典无影响
  2. 创建有序字典,通过collections模块中的OrderDict方法创建
  3. 通过re模块匹配url时,记得添加开始和结束终止符
  4. 激活子菜单时,父级菜单的hide属性要取消掉

mluti_menu()

def multi_menu(request):
    """
    创建二级菜单
    :return:
    """
    menu_dict=request.session[settings.MENU_SESSION_KEY]
    #对字典key进行排序
    key_list=sorted(menu_dict)
    #创建一个空的有序字典
    ordered_dict=OrderedDict()
    for key in key_list:
        val=menu_dict[key]
        val['class']='hide'
        for per in val['children']:
            xrage='^%s$' % per['url']
            if re.match(xrage,request.path_info):
                val['class']=''
                per['class']='active'
        ordered_dict[key]=val
    return {"menu_dict":ordered_dict}
View Code

multi_menu.html

<div class="multi-menu">
    {% for item in menu_dict.values %}
        <div class="item">
            <div class="title">
                <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span>{{ item.title }}
            </div>
            <div class="body {{ item.class }}">
                {% for per in item.children %}
                    <a href="{{ per.url }}" class="{{ per.class }}">{{ per.title }}</a>
                {% endfor %}
            </div>
        </div>
    {% endfor %}
</div>
View Code

BUG处理:

当点击非菜单权限时,菜单无法默认选中或展开:指定不能为菜单的权限到一个可以成为菜单的权限下,并试其选中并展开

  1. 修改数据库结构,添加pid列,related_name参数
    class Permission(models.Model):
        """
        权限表
        """
        title = models.CharField(verbose_name='标题', max_length=32)
        url = models.CharField(verbose_name='含正则的URL', max_length=128)
        # icon=models.CharField(verbose_name='菜单图标',max_length=32,null=True,blank=True)
        # is_menu=models.BooleanField(verbose_name='是否可以做菜单',default=False)
        menu=models.ForeignKey(verbose_name='所属菜单',to='Menu',null=True,blank=True,help_text='null表示非菜单,非null表示二级菜单',on_delete='CASCADE')
        pid=models.ForeignKey(verbose_name='管理的权限',related_name='parents',to='Permission',help_text='对于非菜单权限需要选择一个可以成为菜单的权限,用户做默认展开和选中菜单',null=True,blank=True,on_delete='CASCADE')
        def __str__(self):
            return self.title
    View Code
  2. 修改数据,让不能做菜单的权限和能做菜单的权限进行关联

  3. 在登录后进行权限初始化时,给菜单添加相应的ID,以及给不能做菜单的权限添加PID
    from django.conf import settings
    def init_permission(current_user,request):
        """
        2、权限初始化
        根据当前用户信息获取该用户所有权限信息和菜单信息,并放入session
        role_list=current_user.roles.all()
        permissions__isnull=False过滤没有分配权限的角色
        :param current_user: 当前登录用户
        :param request: 请求的request
        :return:
        """
        permission_queryset = current_user.roles.filter(permissions__isnull=False).values("permissions__id",
                                                                                          "permissions__title",
                                                                                          "permissions__url",
                                                                                          "permissions__pid_id",
                                                                                          "permissions__menu_id",
                                                                                          "permissions__menu__title",
                                                                                          "permissions__menu__icon",
                                                                                          ).distinct()
        permission_list = []
        menu_dict={}
        for item in permission_queryset:
            permission_list.append({'id':item['permissions__id'],'url':item['permissions__url'],'pid':item['permissions__pid_id']})
            menu_id=item.get('permissions__menu_id')#null表示非菜单,非null表示二级菜单
            if not menu_id:
                continue
            node={'id':item['permissions__id'],'title':item['permissions__title'],'url':item['permissions__url']}
            if menu_id in menu_dict:
                menu_dict[menu_id]['children'].append(node)
            else:
                menu_dict[menu_id]={
                        'title': item['permissions__menu__title'],
                        'icon': item['permissions__menu__icon'],
                        'children': [node, ]
                    }
        request.session[settings.PERMISSION_SESSION_KEY] = permission_list
        request.session[settings.MENU_SESSION_KEY] = menu_dict
    View Code
  4. 再次进入时,在中间件中验证权限,获取访问权限路径的id或pid,优先获取pid,有pid表示常规连接,此时的pid就是对应二级菜单的id,若pid为空,那么点击的就是二级菜单,获取到的就是二级连接的id,并将该id或pid放入request参数中
    from django.utils.deprecation import MiddlewareMixin
    from django.shortcuts import HttpResponse
    import re
    from django.conf import settings
    class RbacMiddleware(MiddlewareMixin):
        """
        用户权限信息校验
        """
        def process_request(self,request):
            """
            当用户请求进入时,触发
            1.获取当前请求url
            2.在session数据库中获取当前用户权限列表
            3.匹配url列表,即匹配权限
            :param request:
            :return:
            """
            #白名单URL,无需登录验证都可访问
            valid_url_list=settings.VALID_URL_LIST
            current_url=request.path_info
            for valid_url in valid_url_list:
                reg='^%s$' % valid_url
                if re.match(reg,current_url):
                    return None#在白名单中,无需进行验证,直接返回None
    
            permission_list=request.session.get(settings.PERMISSION_SESSION_KEY)#获取当前用户的权限列表
            if not permission_list:
                return HttpResponse('未获取到用户权限信息,请登录')
            #通过正则匹配url,需要开始于结束符号
            flag=False
            for item in permission_list:
                reg='^%s$' % item['url']
                if re.match(reg,current_url):
                    flag=True#拥有权限访问,不做任何处理
                    request.current_selected_permission=item['pid'] or item['id']
                    break
            if not flag:
                return HttpResponse('无权访问')
    View Code
  5. 过中间件后,通过inclusion_tags来生成菜单。先获取中间件存放在request参数中的id,这个id对应的菜单就是需要默认被选中的,循环子菜单中的url并获取该url的id和request参数中传来的进行对比,比对成功则给该url添加active的样式,给父级的class设置空
    from django.template import Library
    from django.conf import settings
    from collections import OrderedDict
    import re
    register=Library()
    
    @register.inclusion_tag('rbac/multi_menu.html')
    def multi_menu(request):
        """
        创建二级菜单
        :return:
        """
        menu_dict=request.session[settings.MENU_SESSION_KEY]
        #对字典key进行排序
        key_list=sorted(menu_dict)
        #创建一个空的有序字典
        ordered_dict=OrderedDict()
        for key in key_list:
            val=menu_dict[key]
            val['class']='hide'
            for per in val['children']:
                # xrage='^%s$' % per['url']
                # if re.match(xrage,request.path_info):
                #     val['class']=''
                #     per['class']='active'
                if per['id']==request.current_selected_permission:
                    val['class'] = ''
                    per['class'] = 'active'
            ordered_dict[key]=val
        return {"menu_dict":ordered_dict}
    View Code

开发点击导航条

1、在权限列表中添加上级菜单的title和url

2、在中间件中创建一个url_record,并放入request参数中

3、创建inclusion_tags,根据中间件的url_record创建导航条

4、新建inclusion_tags模板

<div>
    <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">
        {% for item in request.url_record %}
            {% if item.class %}
                <li class="{{ item.class }}">{{ item.title }}</li>
            {% else %}
                <li><a href="{{ item.url }}">{{ item.title }}</a></li>
            {% endif %}
        {% endfor %}
    </ol>
</div>
View Code

5、在layout.html中应用inclusion模板即可

 

posted @ 2018-08-28 17:22  丫丫625202  阅读(126)  评论(0编辑  收藏  举报