权限组件
权限控制
创建表格
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
权限控制步骤
- 登录页面是否有访问权限
- POST请求,用户登录验证是否合法
- 获取当前用户相关所有权限,并放入session
- 再次请求,后端编写中间件对用户访问的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>
在业务系统中实现登录逻辑
登录视图模块
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/')
登录成功后将跳转到指定页面,同步更新数据库中的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('无权访问')
完善权限控制,将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
- 在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/')
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', ]
- 将中间件里面的白名单写入setting文件中去
########rbac权限相关配置######## PERMISSION_SESSION_KEY='permission_url_list_key' VALID_URL_LIST=[ '/login/', '/admin/.*' ]
动态菜单
一级菜单
初级菜单实现
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)
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
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>
通过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}
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>
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>
在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}
二级菜单
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/'}, ] }, }
开发二级菜单
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
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>
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}
4、创建rbac相关静态文件rbac.js和rbac.css
rbac.js
$(function(jq){ jq('.multi-menu .title').click(function () { $(this).next().toggleClass('hide'); }); });
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; }
中间件验证用户权限
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("无权访问")
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>
PS:访问的时候可能找不到对应的静态文件,在setting文件中添加
STATIC_ROOT=os.path.join(BASE_DIR,'static')
根据当前访问URL确定是否展开和激活二级菜单
知识点:
- 对字典进行排序,字典排序是对key排序,对原字典无影响
- 创建有序字典,通过collections模块中的OrderDict方法创建
- 通过re模块匹配url时,记得添加开始和结束终止符
- 激活子菜单时,父级菜单的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}
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>
BUG处理:
当点击非菜单权限时,菜单无法默认选中或展开:指定不能为菜单的权限到一个可以成为菜单的权限下,并试其选中并展开
- 修改数据库结构,添加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
- 修改数据,让不能做菜单的权限和能做菜单的权限进行关联
- 在登录后进行权限初始化时,给菜单添加相应的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
- 再次进入时,在中间件中验证权限,获取访问权限路径的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('无权访问')
- 过中间件后,通过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}
开发点击导航条
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>
5、在layout.html中应用inclusion模板即可