Django实战【六】—权限管理系统rbac组件实现
一、权限管理rbac组件
1.权限管理组件的实现思路
表结构分析
rbac的意思之前我详细提过,就是基于角色的访问权限控制,其实说白了啊,就是针对不同的用户角色, 给他们分配了访问哪些url的权利,因为在实际工作场景中,不同分工的人之间的业务也应该是各自来展开的。
也就是说权限本质上是一个url访问路径,而在我们实现的rbac组件中,权限是分配到对应的角色下,然后角色和用户之间又是一层多对多的关系。
有人会问,既然你是想要给用户分配不同的权限,那么为什么不直接单独给用户来分配权限,而是要通过角色表来呢?其实这里涉及到的是一种编程思想。
- 如果我们用户数量很多,权限也很多,那么这么多用户,每一个用户都需要单独分配不同的权限,每个用户的权限也可能很多,这样在权限分配的时候会很繁琐。
- 而通过角色表,将不同角色的权限进行统一的划分,即时你用户再多,权限再多,角色数量总归不会太多。这个时候我们只需要第一次指定好权限和角色之间的对应关系,以后只需要关注用户和角色之间的关系,这样就大大的减少了权限分配业务量,而且整体的关系也更加明朗。
实现思路
- 访问权限
由于我们是要对每个用户的权限来限制,我们想一想什么地方能够做到呢?如果你django基础扎实,那么你很快就能想到中间件,中间件的process_request方法就是对所有过来的请求做一些全局的处理。
那么我们又该如何是每个用户的权限有区分呢?这种因角色不同而不同的数据肯定不可能通过全局来存储,肯定是将对应的权限信息存放在自己独有的空间里,这样的话我们就不难想到session,还记的session把?每个用户用来保存标识客户端的信息,其实session还可以存储其他的信息,比如我们这里的用户权限信息等等。
- 菜单权限
权限验证思路有了,现在的具体情况是如果某个用户有某个权限,我们让他顺利访问,如果他没有某个权限,但是他有这个url权限的链接显示,结果我们给人家展示了一个大黄页,你没有权限。想想都角色有点脑残是吧!用户体验极差,卷铺盖回家把。
那么这里改怎么处理呢,其实在给用户权限的时候,也应当分配好用户所拥有的菜单权限,也就是是够给用户展示的权限。具体的实现就是根据用户权限,来确定他对应拥有的菜单权限,从而在前端页面展示出来。
- 访问权限和菜单权限的注入
访问权限和菜单权限思路都有了,就涉及到什么时候注入了,我们知道权限是针对用户来的,那么这些权限的注入当然也是应该在用户登录成功的一瞬间就注入到这次会话的session中,这样大致的过程也就迎刃而解了。
2.前期页面准备
为了展示二级菜单效果,我们增加了一个私户展示页面,和公户页面同属于客户信息展示下的二级菜单。班级课程记录展示和学员学习记录展示属于教学信息展示页面下二级菜单。
同时为了显示没有子菜单的一级菜单效果,还增加了一个主页url权限,主页没有子菜单,而且主页所有人都可以访问。
主页url
我写在项目urls中,也就是ObCRM/urls.py中
from customer.views import customer urlpatterns = [ # 主页url url(r'^index/', customer.Index.as_view(),name="index"), ]
主页视图函数
主页视图我放在customer下views/customer.py中了。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 主页 class Index(views.View): @method_decorator(login_required) # 装饰器函数验证是否登录 def dispatch(self, request, *args, **kwargs): res = super().dispatch(request, *args, **kwargs) return res def get(self,request): # 展示主页 return render(request,"index.html")
主页html文件
这个主页我随便拷贝的模板,没有具体内容,继承的也是base页面,写在customer应用下templates中。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{% extends 'BASE.html' %}
{% block head %}
{{ block.super }}
{% endblock head %}
{% block title %}
欢迎使用AliCRM系统
{% endblock title %}
{% block content %}
这是主页
{% endblock content %}
{% block js %}
{{ block.super }}
{% endblock js %}
私户展示url
customer应用下urls中
# 私户数据展示 url(r'^private/list/', customer.PrivateList.as_view(), name="private_list"),
私户展示视图
私户展示我就简单实现了搜索,添加,编辑,删除等都是同样的,所以这里就不再实现了。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 私户数据展示 class PrivateList(views.View): @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): res = super().dispatch(request, *args, **kwargs) return res def get(self, request): condition = request.GET.get("condition", "") query = request.GET.get("q", "") condition = condition + "__contains" q = Q() # Q实例化生成q对象,q对象可以帮我们拼接字符串为 condition__contians= xx的关键字参数传到filter中。 q.children.append((condition, query)) if condition and query: # 如果有查询调参数,两个参数都有,根据查询参数查询后找到数据 all_customers = models.Customer.objects.filter(q, consultant=request.user).order_by("-pk") else: # 判断有没有查询参数,只有有一个没有参数,就查询所有公户数据 all_customers = models.Customer.objects.filter(consultant=request.user).order_by("-pk") # 开始分页展示 data_counts = all_customers.count() # 生成一个分页对象 paginator = Paginator(request, data_counts, 10) # 获取当前页展示数据的范围 try: # 异常是否查到了数据,查到了才切片,不然会报错 all_customers = all_customers[paginator.start:paginator.end] except Exception: pass # 获取分页的标签 paginator_tag = paginator.paginate() # 调用定义好的分页方法 # 获取跳转页的标签 jump_tag = paginator.jump_page() # 调用定义好的跳转页方法 return render(request, "private_list.html", {"all_customers": all_customers, "paginator_tag": paginator_tag, "jump_tag": jump_tag})
私户展示html
私户展示html,与公户区别不大。customer应用下templates中
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{% extends 'BASE.html' %} {% load static %} {% block head %} {{ block.super }} {% endblock head %} {% block title %} 私户信息展示 {% endblock title %} {% block content %} <div class="row"> <div class="col-xs-12"> <div class="box"> <div class="box-header"> <h3 class="box-title"></h3> <form action="" method="get" class="navbar-form navbar-left"> <div class="input-group"> <div class="input-group-btn btn-info"> <select name="condition" id="search" class="btn input-group-sm btn-info" style="border: 0"> <option value="" readonly>条件</option> <option value="qq_name">昵称</option> <option value="qq">QQ号</option> </select> </div> <input type="text" name="q" class="form-control" placeholder="Search..."> <span class="input-group-btn"> <button type="submit" id="search-btn" class="btn btn-flat"> <i class="fa fa-search"></i> </button> </span> </div> </form> </div> <div class="box-body"> <div class="row"> <div class="col-sm-6"> </div> </div> <form action="" method="post"> {% csrf_token %} <div class="input-group" style="width: 220px;margin-bottom: 5px;margin-left: 15px"> <select name="operate" id="operate" class="form-control btn-default"> <option value="">选择批量操作</option> <option value="batch_delete">批量删除</option> <option value="batch_update">批量更改客户状态</option> {% if flag %} <option value="batch_c2p">批量公转私</option> {% else %} <option value="batch_c2p">批量公转私</option> {% endif %} </select> <span class="input-group-btn"> <button type="submit" class="btn btn-warning btn-flat">Go!</button> </span> </div> {% if name_str %} <div class="btn text-danger" id="choose_error">顾客:{{ name_str }}已经被选走了</div> {% endif %} <table id="example2" class="table table-bordered table-hover text-center"> <thead> <tr> <th style="width: 6%"> <span> <i class="fa fa-check-square-o"></i> <input type="checkbox" name="batch_choose"> </span> </th> <th style="width: 5%">序号</th> <th>qq</th> <th>姓名</th> <th>电话</th> <th>来源</th> <th>咨询课程</th> <th>客户状态</th> <th>销售老师</th> <th>操作</th> </tr> </thead> <tbody> {% for customer in all_customers %} <tr> <td><input type="checkbox" name="choose" value="{{ customer.pk }}"></td> <td>{{ forloop.counter }}</td> <td>{{ customer.qq }}</td> <td>{{ customer.qq_name }}</td> <td> {{ customer.phone|default:"暂无" }} </td> <td>{{ customer.get_source_display|default:'暂无' }}</td> <td>{{ customer.get_course_display|default:"暂无" }}</td> <td>{{ customer.get_status_display }}</td> <td>{{ customer.consultant.username|default:'暂无' }}</td> <td> <a style="color: #00c3cc;" href="{% url 'common_edit' customer.pk %}"> <i class="fa fa-edit" aria-hidden="true"></i> </a> | <a style="color: #d9534f;" href="{% url 'common_del' customer.pk %}"> <i class="fa fa-trash-o"></i> </a> </td> </tr> {% endfor %} </tbody> <tfoot> </tfoot> </table> {% if not all_customers %} <h3 class="text-center">没有相关记录!</h3> {% endif %} </form> <div class="pull-right" style="display:inline-block; width: 120px;margin: 22px 10px"> {{ jump_tag|safe }} </div> <div class="pull-right"> {{ paginator_tag|safe }} </div> </div> <!-- /.box-body --> </div> <!-- /.box --> </div> <!-- /.col --> </div> {% endblock content %} {% block js %} {{ block.super }} {% endblock js %} {% block customjs %} <script> $("[name=batch_choose]").click(function () { var status = $(this).prop("checked"); $("[name=choose]").prop('checked', status) }); $("#choose_error").click(function () { $("#choose_error").css("display", "none"); }) </script> {{ jump_js|safe }} {% endblock customjs %}
没有分配权限时页面结构如下:
3.权限和菜单代码实现
rbac表结构在看一下
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from django.db import models # Create your models here. from django.db import models from django.contrib.auth.models import AbstractUser # Create your models here. # 身份分类 role_choices = ( ("1", "董事"), ("2", "CEO"), ("3", "销售"), ("4", "网咨"), ("5", "老师"), ("6", "班主任"), ) # 扩展的用户表 class UserInfo(AbstractUser): """用户信息表:老师,助教,销售,班主任""" id = models.AutoField(primary_key=True) gender_type = (("male", "男"), ("female", "女")) gender = models.CharField(choices=gender_type, null=True, max_length=12) phone = models.CharField(max_length=11, null=True, unique=True) role = models.ManyToManyField("Role") def __str__(self): return self.username # 身份表 class Role(models.Model): title = models.CharField("职位", choices=role_choices, max_length=32) permission = models.ManyToManyField("Permission") def __str__(self): return self.title # 权限表 class Permission(models.Model): name = models.CharField(max_length=32, verbose_name=u'权限名') url = models.CharField( max_length=300, verbose_name=u'权限url地址', null=True, blank=True, help_text=u'是否给菜单设置一个url地址' ) icon = models.CharField( max_length=32, verbose_name='权限图标', null=True, blank=True ) # 指定属于哪个父级权限 parent = models.ForeignKey( 'self', verbose_name=u'父级权限', null=True, blank=True, help_text=u'如果添加的是子权限,请选择父权限' ) # 指定属于哪个menu menu = models.ForeignKey(to="Menu",verbose_name=u'对应菜单',blank=True,null=True) def __str__(self): return "{parent}{name}".format(name=self.name, parent="%s-->" % self.parent.name if self.parent else '') class Meta: verbose_name = u"权限表" verbose_name_plural = u"权限表" ordering = ["id"] # 菜单表 class Menu(models.Model): title = models.CharField(max_length=32, verbose_name=u'菜单名') # 菜单显示图标 icon = models.CharField( max_length=32, verbose_name='菜单图标', null=True, blank=True ) # 指定属于哪个父级菜单 parent = models.ForeignKey( 'self', verbose_name=u'父级菜单', null=True, blank=True, help_text=u'如果添加的是子菜单,请选择父菜单' ) priority = models.IntegerField( verbose_name=u'显示优先级', null=True, blank=True, help_text=u'菜单的显示顺序,优先级越小显示越靠前' ) def __str__(self): return "{parent}{title}".format(title=self.title, parent="%s-->" % self.parent.title if self.parent else '') class Meta: verbose_name = u"菜单表" verbose_name_plural = u"菜单表" ordering = ["priority","id"] # 根据优先级和id来排序
首先我们插入一些模拟的数据,数据如下:
其实用户角色,角色权限之间的关系结构如下
中间件验证权限
权限认证我们之前想出的解决方法,是通过中间件对所有请求进行权限验证,并设置白名单,放行某一些通用的权限。
白名单配置
在项目下settings文件中配置白名单
# 配置白名单 WHITE_URL_LIST = [ r'^/admin/.*', # 放行admin应用url r'^/rbac/login/', r'^/rbac/register/', r'^/rbac/get_auth_img/', ]
中间件写法
在rbac应用下新建middlewares文件下的permission.py中自定义我们的中间件,用来验证用户登录和用户权限,之后设计的面包屑也是在这里面设置,这个之后再说。
import re from django.shortcuts import HttpResponse,redirect,render from django.utils.deprecation import MiddlewareMixin from django.conf import settings class PermissionMiddleware(MiddlewareMixin): """自定义权限分配中间件""" def process_request(self,request): # 对权限进行校验 # 1. 当前访问的URL在不在白名单 for i in settings.WHITE_URL_LIST: ret = re.search(i,request.path) if ret: return None # 获取当前用户的所有权限 user = request.user if not user: return redirect("login") # 获取用户权限列表 permissions_list = request.session.get("permissions_list") if permissions_list: for permission in permissions_list: # 遍历权限列表,匹配当前路径,匹配上放行 url = permission['url'] if re.search(f"^{url}",request.path): # 请求子权限路径,父级权限和父级菜单激活样式设置 request.show_id = permission["parent_id"] return None # 没有匹配上,提示没有权限 return HttpResponse("没有权限")
在settings中注册中间件,中间件才会生效。
MIDDLEWARE = [ 'rbac.middlewares.permission.PermissionMiddleware' # 配置验证用户和用户权限的中间件 ]
用户权限和菜单权限注入
既然配置了中间件,那么必然需要给用户注入他有的权限,权限注入在用户登录成功后一瞬间注入。我们在rbac下新建一个severce文件夹,文件夹下新建init_permission.py,定义权限注入函数。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from rbac import models def init_permission(request, user): """ 在登录函数验证通过后,在session中注入用户权限和用户菜单权限。 :param request: 用户登录请求时的wsgi请求对象 :param user: 用户登录验证通过后的用户账号名 :return: none, """ permissions = models.Permission.objects.filter(role__userinfo__username=user).values("pk", "name", "url", "icon","parent_id", "menu__pk","menu__title", "menu__icon","menu__priority","menu__parent__id").order_by("menu__priority").distinct() # print(permissions) permissions_list = [] # 定义权限列表 menus_dict = {} # 定义菜单列表 print("当前用户权限>>>") for permission in permissions: # 遍历权限列表 # 获取用户权限的数据结构,列表套字典,一个字典代表一个权限 permissions_list.append({ "pk": permission["pk"], "name": permission["name"], "url": permission["url"], "parent_id": permission["parent_id"], }) print(permission["name"].center(8, " "), ":", permission["url"]) # 获取菜单权限的数据结构,字典套字典,一个字典代表一个菜单 if permission["menu__pk"]: # 如果父级菜单已存在,在儿子列表中添加 if permission["menu__pk"] in menus_dict: # ruguo menus_dict[permission["menu__pk"]]["children"].append({ "pk": permission["pk"], "name": permission["name"], "url": permission["url"], "icon": permission["icon"], "parent_id": permission["parent_id"], }) else: # 父级菜单不存在,则添加一个父级菜单。 menus_dict[permission["menu__pk"]] = { "pk": permission["menu__pk"], "title": permission["menu__title"], "icon": permission["menu__icon"], "parent_id": permission["menu__parent__id"], "priority": permission["menu__priority"], "children": [{ "pk": permission["pk"], "name": permission["name"], "url": permission["url"], "icon": permission["icon"], "parent_id": permission["parent_id"], }] # 定义一个父级菜单包含所有儿子菜单的空列表 } # print("权限列表",permissions_list) # print("菜单权限",menus_dict) # session中注入权限数据 request.session["permissions_list"] = permissions_list # session中注入菜单数据 request.session["menus_dict"] = menus_dict
权限注入函数在登录视图验证成功后调用
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from rbac.service import init_permission # 用户登录视图类 class Login(views.View): def get(self, request): # get请求返回登录页面 return render(request, "login.html") def post(self, request): data = request.POST # 获取用户登录信息 authcode = data.get("authcode") username = data.get("username") password = data.get("password") # 验证码不正确 if request.session.get("authcode").upper() != authcode.upper(): return JsonResponse({"status": "1"}) else: print(username) # 使用django的auth模块进行用户名密码验证 user = auth.authenticate(username=username, password=password) if user: # 将用户名存入session中 request.session["user"] = username auth.login(request, user) # 将用户对象存入request对象的属性中 init_permission.init_permission(request, user) # 调用权限注入函数,注入用户权限 return JsonResponse({"status": "2"}) else: return JsonResponse({"status": "3"})
自定义标签来生成菜单
通过权限注入和菜单注入后,我们可以在session中获取用户的菜单权限数据,还记得我们的inclusion_tag吧!这里简单回顾一下。
inclusion_tag用来装饰一个函数成为自定义标签,把函数的返回值放在字典里面,调用了render方法渲染到指定的html文件中,并把这个html文件在前端调用inclusion_tag的位置当成组件使用。
说完那就看代码吧!
- 在rbac应用下templates中新建menu.html
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> {% for menu in menus_dict.values %} {% if menu.children|length == 1 %} <li class="{{ menu.class }}"> <a href="{{ menu.children.0.url }}"> <i class="fa {{ menu.icon }}"></i> <span>{{ menu.title }}</span> <span class="pull-right-container"> </span> </a> </li> {% else %} <li class="treeview {{ menu.class }}"> <a href=""> <i class="fa {{ menu.icon }}"></i> <span>{{ menu.title }}</span> <span class="pull-right-container"> <span class="fa fa-angle-left pull-right"></span> </span> </a> <ul class="treeview-menu"> {% for child_menu in menu.children %} <li class="{{ child_menu.class }}"> <a href="{{ child_menu.url }}"> <i class="fa fa-circle-o"></i> {{ child_menu.name }} </a> </li> {% endfor %} </ul> </li> {% endif %} {% endfor %} <script></script> </body> </html>
- rbac应用下新建templatetags文件夹(名字固定不可变)中新建rbac.py,代码写法如下
import re from django import template register = template.Library() @register.inclusion_tag("menu.html") def get_menu(request): menus_dict = request.session.get("menus_dict") for menu in menus_dict.values(): # 遍历菜单 for child in menu.get("children"): # 遍历子菜单的children列表 if re.match(child["url"],request.path) or request.show_id == child["pk"]: # 对当前路径进行匹配,匹配上了给二级菜单加上激活样式,同时给父级菜单也加上激活样式,或者当前路径是某个二级菜单权限的子权限 menu["class"] = "active" child["class"] = "active" return {"menus_dict": menus_dict}
这里我们做了一个样式激活处理,就是判断当前来的请求和菜单中的url去匹配,匹配上为这个二级菜单以及父级菜单加上激活样式"active"。
- 前端base页面使用生成菜单标签
<ul class="sidebar-menu" data-widget="tree"> <li class="header">操作菜单</li> {% load rbac %} {% get_menu request %} </ul>
动态菜单实现后效果演示