权限: 一个含正则表达式的url
rabc使用步骤 (1) 先将rbac组建移植到新的项目中 (2) 将settings中install_app中加入"rbac" (3) 将新项目的用户表与rbac下的User表一对一关联 (4) 数据迁移 (5) 在登录成功后引入rbac下的initial_session方法,做登录用户的权限信息存储(注意user对象) (6) 在settings中引入rbac下的权限校验中间件 (7) 在项目的base模板中引入菜单样式,渲染显示
1. 基于RBAC设计表关系:
注意:在权限表中加入is_menu和icon两个字段用来表示该权限是否是(查看)菜单权限
from django.db import models from django.contrib.auth.models import AbstractUser class Menu(models.Model): title = models.CharField(max_length=32, verbose_name='菜单') icon = models.CharField(max_length=32, verbose_name='图标', null=True, blank=True) def __str__(self): return self.title class Permission(models.Model): """ 权限表 """ title = models.CharField(max_length=32, verbose_name='标题') url = models.CharField(max_length=32, verbose_name='权限') menu = models.ForeignKey("Menu", on_delete=models.CASCADE, null=True, blank=True) name = models.CharField(max_length=32, verbose_name='url别名', default="", blank=True) pid = models.ForeignKey("self", on_delete=models.CASCADE, null=True, verbose_name='父权限', blank=True) # is_menu = models.BooleanField(default=False, verbose_name='是否是菜单') # icon = models.CharField(max_length=32, verbose_name='图标', null=True, blank=True) class Meta: verbose_name_plural = '权限表' verbose_name = '权限表' def __str__(self): return self.title class Role(models.Model): name = models.CharField(max_length=32, verbose_name='角色名称') permissions = models.ManyToManyField(to='Permission', verbose_name='角色所拥有的权限', blank=True) def __str__(self): return self.name # 1.在应用app的models中的用户表继承User,如下: # from rbac.models import User # class UserInfo(User): # """ # 员工表 # """ # birthday = models.DateField(verbose_name='生日', blank=True, null=True) # age = models.IntegerField(verbose_name='年龄', blank=True, null=True) # image = models.ImageField(upload_to='images/%Y/%m', verbose_name='头像', null=True, blank=True) # depart = models.ForeignKey("Department", on_delete=models.CASCADE, null=True, blank=True) # def __str__(self): # return self.name # # class Meta: # verbose_name = '员工表' # verbose_name_plural = '员工表' # 2.在settings中配置应用app的继承User的表,这样会生成一张继承User的crm_userinfo表: # AUTH_USER_MODEL ="crm.UserInfo" # 注:User中的class Meta: abstract = True 表示不会生成User这张表 # 在某个功能函数中验证是否登录加装饰器:@login_required # 注:登录验证只需如下: # def login(request): # if request.is_ajax(): # res = {'user': None, 'err_msg': ''} # user = request.POST.get('user') # pwd = request.POST.get('pwd') # validcode = request.POST.get('validcode') # if validcode.upper() == request.session.get('keep_str').upper(): # user_obj = auth.authenticate(username=user, password=pwd) # # if user_obj: # auth.login(request, user_obj) # request.session['user_id'] = user_obj.pk # initial_sesson(user_obj, request) # res['user'] = user # else: # res['err_msg'] = '用户名或者密码错误!' # else: # res['err_msg'] = '验证码错误' # return JsonResponse(res) # else: # return render(request, 'login.html') class User(AbstractUser): """ 用户表 """ name = models.CharField(max_length=32, verbose_name='用户名') password = models.CharField(max_length=32, verbose_name='密码') roles = models.ManyToManyField(to='Role', verbose_name='用户所拥有的角色', blank=True) def __str__(self): return self.name class Meta: abstract = True
# 1.在应用app的models中的用户表继承User,如下: # from rbac.models import User # class UserInfo(User): # """ # 员工表 # """ # birthday = models.DateField(verbose_name='生日', blank=True, null=True) # age = models.IntegerField(verbose_name='年龄', blank=True, null=True) # image = models.ImageField(upload_to='images/%Y/%m', verbose_name='头像', null=True, blank=True) # depart = models.ForeignKey("Department", on_delete=models.CASCADE, null=True, blank=True) # def __str__(self): # return self.name # # class Meta: # verbose_name = '员工表' # verbose_name_plural = '员工表' # 2.在settings中配置应用app的继承User的表,这样会生成一张继承User的crm_userinfo表: # AUTH_USER_MODEL ="crm.UserInfo" # 注:User中的class Meta: abstract = True 表示不会生成User这张表 # 在某个功能函数中验证是否登录加装饰器:@login_required # 注:登录验证只需如下: # def login(request): # if request.is_ajax(): # res = {'user': None, 'err_msg': ''} # user = request.POST.get('user') # pwd = request.POST.get('pwd') # validcode = request.POST.get('validcode') # if validcode.upper() == request.session.get('keep_str').upper(): # user_obj = auth.authenticate(username=user, password=pwd) # # if user_obj: # auth.login(request, user_obj) # request.session['user_id'] = user_obj.pk # initial_sesson(user_obj, request) # res['user'] = user # else: # res['err_msg'] = '用户名或者密码错误!' # else: # res['err_msg'] = '验证码错误' # return JsonResponse(res) # else: # return render(request, 'login.html')
2. 基于admin录入数据
from django.contrib import admin from rbac import models class PermissionAdmin(admin.ModelAdmin): list_display = ['title', 'url', 'menu'] # list_editable = ['url', 'is_menu', 'icon'] search_fields = ["title"] admin.site.register(models.Permission, PermissionAdmin) admin.site.register(models.Role) admin.site.register(models.Menu)
3. 登录认证
将该用户的权限列表和菜单权限列表注入到session中,(只要一级菜单时使用permission_menu_list=[{},{}],以下为二级菜单示例permission_menu_dict)
from django.shortcuts import render from django.http import JsonResponse from django.contrib import auth from rbac.service.rbac import initial_sesson def login(request): """ 基于ajax和用户认证组件实现的登录功能 :param request: :return: """ if request.is_ajax(): res = {'user': None, 'err_msg': ''} user = request.POST.get('user') pwd = request.POST.get('pwd') validcode = request.POST.get('validcode') if validcode.upper() == request.session.get('keep_str').upper(): user_obj = auth.authenticate(username=user, password=pwd) if user_obj: auth.login(request, user_obj) request.session['user_id'] = user_obj.pk initial_sesson(user_obj, request) res['user'] = user else: res['err_msg'] = '用户名或者密码错误!' else: res['err_msg'] = '验证码错误' return JsonResponse(res) else: return render(request, 'login.html')
from app01.models import Role def initial_sesson(user,request): """ 功能:将当前登录人的所有权限录入session中 :param user: 当前登录人 """ # 查询当前登录人的所有权限列表 # 查看当前登录人的所有角色 # ret=Role.objects.filter(user=user) permissions = Role.objects.filter(user=user).values("permissions__url", "permissions__title", "permissions__name", "permissions__pk", "permissions__pid", "permissions__menu__title", "permissions__menu__icon", "permissions__menu__pk").distinct() print(permissions) permission_list = [] permission_names = [] permission_menu_dict = {} for item in permissions: # 构建权限列表 permission_list.append({ "url": item["permissions__url"], "id": item["permissions__pk"], "pid": item["permissions__pid"], "title": item["permissions__title"], }) #构建别名列表 permission_names.append(item["permissions__name"]) # 菜单权限 menu_pk = item["permissions__menu__pk"] if menu_pk: if menu_pk not in permission_menu_dict: permission_menu_dict[menu_pk] = { "menu_title": item["permissions__menu__title"], "menu_icon": item["permissions__menu__icon"], "children": [ { "title":item["permissions__title"], "url":item["permissions__url"], "pk": item["permissions__pk"] } ], } else: permission_menu_dict[menu_pk]["children"].append({ "title": item["permissions__title"], "url": item["permissions__url"], "pk": item["permissions__pk"] }) print("permission_menu_dict", permission_menu_dict) # permission_menu_list: ''' 元数据: [ { 'permissions__url': '/customer/list/', 'permissions__title': '客户列表', 'permissions__menu__title': '信息管理', 'permissions__menu__icon': 'fa fa-connectdevelop', 'permissions__menu__pk': 1 }, { 'permissions__url': '/mycustomer/', 'permissions__title': '我的私户', 'permissions__menu__title': '信息管理', 'permissions__menu__icon': 'fa fa-connectdevelop', 'permissions__menu__pk': 1 }, { 'permissions__url': '/payment/list/', 'permissions__title': '缴费列表', 'permissions__menu__title': '财务管理', 'permissions__menu__icon': 'fa fa-code-fork', 'permissions__menu__pk': 2 }, ] 目标数据: { 1:{ "title":"信息管理", "icon":"", "children":[ { "title":"客户列表", "url":"", } ] }, 2:{ "title":"财务管理", "icon":"", "children":[ { "title":"缴费列表", "url":"", }, ] }, } permission_menu_dict= { 1:{ "title":"信息管理", "icon":"", "children":[ { "title":"客户列表", "url":"", }, { "title":"我的私户", "url":"", }, ] }, 2:{ "title":"财务管理", "icon":"", "children":[ { "title":"缴费列表", "url":"", }, ] }, } ''' # 将当前登录人的权限列表注入session中 request.session["permission_list"] = permission_list # 将当前登录人的url别名列表注入session中 request.session["permission_names"] = permission_names # 将当前登录人的菜单权限字典注入session中 request.session["permission_menu_dict"] = permission_menu_dict
4. 基于中间件和正则实现权限校验
注意:在settings中配置中间件: "rbac.service.middlewares.PermissionMiddleWare"
from django.utils.deprecation import MiddlewareMixin from django.shortcuts import HttpResponse, redirect import re from app01.models import Permission class PermissionMiddleWare(MiddlewareMixin): def process_request(self,request): current_path = request.path # 设置白名单放行 for reg in ["/login/", "/admin/*"]: ret = re.search(reg, current_path) if ret: return None # /customers/edit/1 # 校验是否登录 user_id = request.session.get("user_id") if not user_id: return redirect("/login/") # 校验权限 permission_list = request.session.get("permission_list") # 路径导航列表 request.breadcrumb = [ { "title": "首页", "url": "/" }, ] for item in permission_list: reg = "^%s$" % item["url"] ret = re.search(reg,current_path) if ret: show_id = item["pid"] or item["id"] request.show_id = show_id # 确定面包屑列表 if item["pid"]: ppermission = Permission.objects.filter(pk=item["pid"]).first() request.breadcrumb.extend( [ # 父权限字典 { "title": ppermission.title, "url": ppermission.url }, # 子权限字典 { "title": item["title"], "url": request.path } ] ) else: request.breadcrumb.append( { "title": item["title"], "url": item["url"] } ) return None return HttpResponse("无访问权限!")
5. 在页面上动态显示菜单权限(包括点击标签加入active样式,包括有相应权限者显示相应权限按钮无相应权限者不显示相应权限按钮)
注意:在base.html中引入bootstrap,font-awesome,menu.css,menu.js添加样式和事件
{% load staticfiles %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路飞学城</title> <link rel="shortcut icon" href="{% static 'imgs/luffy-study-logo.png' %}"> <link rel="stylesheet" href="{% static 'plugins/bootstrap/css/bootstrap.css' %} "/> <link rel="stylesheet" href="{% static 'plugins/font-awesome/css/font-awesome.css' %} "/> <link rel="stylesheet" href="{% static 'css/commons.css' %} "/> <link rel="stylesheet" href="{% static 'css/nav.css' %} "/> <link rel="stylesheet" href="{% static 'css/menu.css' %}"> <style> body { margin: 0; } .no-radius { border-radius: 0; } .no-margin { margin: 0; } .pg-body > .left-menu { background-color: #EAEDF1; position: absolute; left: 1px; top: 48px; bottom: 0; width: 220px; border: 1px solid #EAEDF1; overflow: auto; } .pg-body > .right-body { position: absolute; left: 225px; right: 0; top: 48px; bottom: 0; overflow: scroll; border: 1px solid #ddd; border-top: 0; font-size: 13px; min-width: 755px; } .navbar-right { float: right !important; margin-right: -15px; } .luffy-container { padding: 15px; } .left-menu .menu-body .static-menu { } </style> </head> <body> <div class="pg-header"> <div class="nav"> <div class="logo-area left"> <a href="#"> <img class="logo" src="{% static 'imgs/logo.svg' %}"> <span style="font-size: 18px;">路飞学城 </span> </a> </div> <div class="left-menu left"> <a class="menu-item">资产管理</a> <a class="menu-item">用户信息</a> <a class="menu-item">路飞管理</a> <div class="menu-item"> <span>使用说明</span> <i class="fa fa-caret-down" aria-hidden="true"></i> <div class="more-info"> <a href="#" class="more-item">管他什么菜单</a> <a href="#" class="more-item">实在是编不了</a> </div> </div> </div> <div class="right-menu right clearfix"> <div class="user-info right"> <a href="#" class="avatar"> <img class="img-circle" src="{% static 'imgs/default.png' %}"> </a> <div class="more-info"> <a href="#" class="more-item">个人信息</a> <a href="#" class="more-item">注销</a> </div> </div> <a class="user-menu right"> 消息 <i class="fa fa-commenting-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 通知 <i class="fa fa-envelope-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 任务 <i class="fa fa-bell-o" aria-hidden="true"></i> <span class="badge bg-danger">4</span> </a> </div> </div> </div> <div class="pg-body"> <div class="left-menu"> <div class="menu-body"> {% load rbac %} {% get_menu_styles request %} </div> </div> <div class="right-body"> <div> <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;"> {% for item in request.breadcrumb %} <li><a href="{{ item.url }}">{{ item.title }}</a></li> {% endfor %} </ol> </div> {% block content %} {% endblock %} </div> </div> <script src="{% static 'js/jquery-3.3.1.min.js' %} "></script> <script src="{% static 'plugins/bootstrap/js/bootstrap.js' %} "></script> <script src="{% static 'js/menu.js' %} "></script> {% block js %} {% endblock %} </body> </html>
{% extends 'base.html' %} {% block content %} <div class="luffy-container"> <div class="btn-group" style="margin: 5px 0"> {% load rbac %} {% if "customer_add"|has_permission:request %} <a class="btn btn-default" href="/customer/add/"> <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户 </a> {% endif %} </div> <table class="table table-bordered table-hover"> <thead> <tr> <th>ID</th> <th>客户姓名</th> <th>年龄</th> <th>邮箱</th> <th>公司</th> {% if "customer_edit"|has_permission:request%} <th>编辑</th> {% endif %} {% if "customer_del"|has_permission:request %} <th>删除</th> {% endif %} </tr> </thead> <tbody> {% for row in data_list %} <tr> <td>{{ row.id }}</td> <td>{{ row.name }}</td> <td>{{ row.age }}</td> <td>{{ row.email }}</td> <td>{{ row.company }}</td> {% if "customer_edit"|has_permission:request %} <td> <a style="color: #333333;" href="/customer/edit/{{ row.id }}/"> <i class="fa fa-edit" aria-hidden="true"></i> </a> </td> {% endif %} {% if "customer_del"|has_permission:request %} <td> <a style="color: #d9534f;" href="/customer/del/{{ row.id }}/"> <i class="fa fa-trash-o"></i> </a> </td> {% endif %} </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
...
.static-menu .icon-wrap { width: 20px; display: inline-block; text-align: center; } .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; } .static-menu a:hover { color: #2F72AB; border-left: 2px solid #2F72AB; } .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 a.active { border-left: 2px solid #2F72AB; }
$('.item .title').click(function () { $(this).next().toggleClass('hide'); $(this).parent().siblings().children(".body").addClass("hide") });
在菜单区域(side_bar):渲染出菜单链接标签: {% for item in request.session.permission_menu_list %} <p><a href="{{ item.url }}">{{ item.title }}</a></p> {% endfor %}
点击标签加入active样式: 解决思路1: 在每一个返回母版的视图函数中加上如下代码: permission_menu_list = request.session.get("permission_menu_list") for item in permission_menu_list: if re.search("^{}$".format(item["url"]), request.path): item["class"] = "active" 解决思路2: 引入inclusion_tag方法
引入inclusion_tag方法
<div class="multi-menu"> {% for item in permission_menu_dict.values %} <div class="item"> <div class="title"><i class="{{ item.menu_icon }}"></i>{{ item.menu_title }}</div> <div class="body {{ item.class }}"> {% for foo in item.children %} <a href="{{ foo.url }}">{{ foo.title }}</a> {% endfor %} </div> </div> {% endfor %} </div>
from django.template import Library register =Library() @register.inclusion_tag("rbac/menu.html") def get_menu_styles(request): permission_menu_dict = request.session.get("permission_menu_dict") for val in permission_menu_dict.values(): for item in val["children"]: val["class"] = "hide" # ret=re.search("^{}$".format(item["url"]),request.path) if request.show_id == item["pk"]: val["class"] = "" return {"permission_menu_dict": permission_menu_dict} @register.filter def has_permission(btn_url, request): permission_names = request.session.get("permission_names") return btn_url in permission_names