Django【十七】权限管理与路径导航
1、url权限管理
设计表
1、设计表 系统一共有多少个路径; 有哪些用户使用; 用户在公司的角色; 对角色进行权限分配(什么样的角色可以访问什么样的路径); 2、往表中添加数据,分配角色权限 3、登录成功,设置session,并将该用户可以访问的url从表中取出保存在session中, 4、设置中间件rocess_request,
设置白名单放行登录和admin:
以admin开头的所有路径都放行
# 设置白名单放行 for i in ["/login/","/register/","/logout/","/code/","/admin/.*"]: # 请求的路径能够和i匹配 # 匹配不到返回None print(request.path) ret = re.search(i,request.path) if ret: print(ret) return None
登录认证:
# 登录认证,判断当前用户是否携带session来访问 if not request.user.username: return redirect("login")
权限认证:
session中的路径是用户可以访问的路径,
request.path是当前访问路径,与session中的路径进行匹配,search成功return None
中间件继续往下执行,search不到ret为None,如果当前访问的路径都不在这个列表中证明该用户没有访问这个路径的权限,返回对应的错误(权限不够,无法访问)
# 权限认证 for item in request.session["permisson_list"]: res = "^%s$"%item ret = re.search(res,request.path) if ret: return None return HttpResponse("您的权限不够不能访问该页面!!!")
2、左侧菜单栏权限分配
动态显示左侧一级菜单栏
权限表模型
# 权限表 class Permission(models.Model): title = models.CharField(max_length=32) url = models.CharField(max_length=128) 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
admin显示:
展示的时候直接可以编辑:
显示表的名字:
# 权限表 class Permission(models.Model): title = models.CharField(max_length=32) url = models.CharField(max_length=128) 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 model中的verbose_name_plural
具体流程:
1、获取登录用户拥有的所有权限,用户菜单栏数据(用户权限的url中有哪些是菜单栏); 2、将菜单栏的数据注入到session中; 3、不管是公有客户还是私有客户菜单栏都是通过继承的模板,所以我们只需要在模板中动态生成菜单栏即可,哪些用户有哪些菜单栏; 4、为了加强代码的复用性,我们使用自定义标签来生成左侧菜单栏,,并给标签添加选中样式;
5、自定义标签把我们事先保存在session中的菜单栏数据取出来渲染到指定页面,这里我们用的inclusion_tag("result.html");
6、在后端给选中的标签添加样式,用request.path与我们取出的菜单栏数据的url进行匹配,匹配成功表示这个url就是我们点击的标签,对应的给它添加"class":"active";
用户菜单栏数据注入:
# 判断用户输入的用户名和密码与数据库中的是否匹配 user_obj = auth.authenticate(username=username, password=password) if user_obj and str_code.upper()==code.upper(): # 登录成功设置session auth.login(request, user_obj) # 获取该用户的所有权限并保存在session中,并去重 permissions = models.Permission.objects.filter(role__userinfo__username=user_obj.username).distinct() # 将该用户可以访问的所有url和菜单栏数据都添加到session中 permisson_list = [] permisson_is_menu_list = [] for item in permissions: # 用户权限 permisson_list.append(item.url)
# 如果路径是否是菜单栏 if item.is_menu: # 用户菜单栏数据(用户权限的url中有哪些是菜单栏) permisson_is_menu_list.append({ "title": item.title, "url": item.url, "icon":item.icon, }) request.session["permisson_list"] = permisson_list request.session["permisson_is_menu_list"] = permisson_is_menu_list print("permisson_list-->",permisson_list) print("permisson_is_menu_list-->",permisson_is_menu_list) # permisson_list--> ['/home/', '/customers/'] # permisson_is_menu_list--> [{'title': '首页', 'url': '/home/', 'icon': 'fa fa-link'}, {'title': '公共客户信息', 'url': '/customers/', 'icon': 'fa fa-link'}]
自定义标签:
from django import template register = template.Library() import re @register.inclusion_tag("result.html") def create_label(request): menu_list = request.session.get("permisson_is_menu_list") # 给点击的标签添加选中样式 for item in menu_list: # 请求路径与当前用户的菜单栏数据中的url匹配成功说明这个路径就是当前用户点击的a标签发起的请求 if re.match("^{}$".format(item["url"]),request.path): # match 匹配开头的,匹配失败返回None,匹配成功可通过group(0)返回匹配成功的字符串 # setch 扫描整个字符串,匹配不到返回None item["class"]="active" break # 将菜单栏的数据交给result页面去渲染,选然后将结果当作组件返回给调用create_label的页面 return {"menu_list":menu_list}
inclusion_tag指定的result页面:
{% for foo in menu_list %} <li class="{{ foo.class }}" ><a href="{{ foo.url }}" ><i class="{{ foo.icon }}"></i> <span>{{ foo.title }}</span></a></li> {% endfor %}
模板中引入自定义标签:
<ul class="sidebar-menu" data-widget="tree"> <li class="header">HEADER</li> <!-- Optionally, you can add icons to the links --> {% load mytag %} <!--引用自定义标签--> {% create_label request %} <!--调用自定义标签,并将request传过去--> </ul>
3、二级菜单
二级菜单和一级菜单不同的是:
1、设计表结构添加数据
一级菜单与权限表一对多关联,权限表设置自关联
# 菜单栏 class Menu(models.Model): title = models.CharField(max_length=32,verbose_name='一级菜单',null=True,blank=True) icon = models.CharField(max_length=32, verbose_name='图标', null=True, blank=True) # 权限表 class Permission(models.Model): title = models.CharField(max_length=32) url = models.CharField(max_length=128) menu = models.ForeignKey("Menu",null=True) pid = models.ForeignKey("self",null=True) # 自关联 class Meta: verbose_name_plural="权限表" verbose_name = '权限表' def __str__(self): return self.title
2、登陆成功后获取当前登录用户的所有权限,将用户所有可以访问的url和菜单栏数据保存在session中
# 获取该用户的所有权限并保存在session中,并去重 permissions = models.Permission.objects.filter(role__userinfo__username=user_obj.username).values("title","url","pk","pid","menu__title","menu__icon","menu_id").distinct() # 将该用户可以访问的所有url和菜单栏数据都添加到session中 input_permission(request, permissions
def input_permission(request, permissions): # 将该用户可以访问的所有url和菜单栏数据都添加到session中 permisson_list = [] permisson_is_menu_dict = {} for item in permissions: # 用户权限 permisson_list.append({ "pk":item["pk"], "url":item["url"], "pid":item["pid"], }) # 判断是否是一级菜单 if item["menu_id"]: # menu_id同属于一个 一级菜单(下一个a标签的menu_id也是当前的一级标签) if item["menu_id"] in permisson_is_menu_dict: permisson_is_menu_dict[item["menu_id"]]["children"].append({ "pk":item["pk"],"title": item["title"], "url": item["url"] }) else: # 设计菜单栏数据结构(方便前端渲染) permisson_is_menu_dict[item["menu_id"]] = { "title": item["menu__title"], "icon": item["menu__icon"], "children": [ { "pk":item["pk"],"title": item["title"], "url": item["url"]}, ] } request.session["permisson_list"] = permisson_list request.session["permisson_is_menu_dict"] = permisson_is_menu_dict
中间件取提取权限列表进行验证,并将当前访问路径的pid封装在request中
import re from django.shortcuts import HttpResponse,redirect,render from django.utils.deprecation import MiddlewareMixin class PermissonMiddleware(MiddlewareMixin): def process_request(self,request): # 设置白名单放行 for i in ["/login/","/register/","/logout/","/code/","/admin/.*"]: # 请求的路径能够和i匹配 # 匹配不到返回None ret = re.search(i,request.path) if ret: return None # 登录认证,判断当前用户是否携带session来访问 if not request.user.username: return redirect("login") # 权限认证 for item in request.session["permisson_list"]: res = "^%s$"%item["url"] ret = re.search(res,request.path) if ret: # 当前访问路径的pid 给request对象动态添加了个属性 request.show_id = item["pid"] return None return HttpResponse("您的权限不够不能访问该页面!!!")
3、菜单栏的数据我们通过自定义标签来渲染
根据我们构建的数据格式在前端进行渲染
'1': { 'title': '信息管理', 'icon': 'fa fa-link', 'children': [{ 'pk': 5, 'title': '首页', 'url': '/home/' }, { 'pk': 6, 'title': '公共客户信息', 'url': '/customers/' }, { 'pk': 7, 'title': '我的客户', 'url': '/my_customers/' }] },
自定义标签
from django import template register = template.Library() @register.inclusion_tag("result.html") def create_label(request): menu_dict = request.session.get("permisson_is_menu_dict") for key, item in menu_dict.items(): # 默认给二级标签添加隐藏样式 item["class"] = "hide" for child in item["children"]: # if re.match("^{}$".format(child["url"]), request.path): # 当前访问路径的pid如果等于它二级菜单的id证明他俩是属于同一个一级菜单, # 如果是属于同一个一级菜单,我们就让这个二级菜单一直显示,并给它添加选中样式; if request.show_id == child["pk"]: item["class"] = "" child["class"] = "active" break return {"menu_dict" :menu_dict }
我们通过权限表自关联的方式来区分他们的从属关系;
bug:通过正则匹配无法确定从属关系
那么我们为什么要区分他们的从属关系呢: 默认二级标签是隐藏状态的,我们为了在点击我的客户或当前一级标签的其他二级标签时都是展开的状态,我们通过正则当前请求的路径request.path与二级菜单进行匹配,匹配成功证明当前用户请求的url在二级菜单中,我们设置它父级标签的class属性值为空,让其展开; 当我们点击我的客户中的其他功能时,因为这些功能的路径都不在二级菜单中,所以无法通过正则的方式进行匹配,也就是说我们在点击这些功能时左侧的菜单栏会隐藏起来,为了避免这种bug出现我们扩展了权限表添加了pid字段,并通过自关联的方式解决了bug
当前请求路径的pid与菜单栏id进行判断:
result.html生成二级标签
<div class="multi-menu"> {% for item in menu_dict.values %} <div class="item"> <div class="title"> <i class="{{ item.icon }}"></i>{{ item.title }} </div> <div class="body {{ item.class }}"> {% for child in item.children %} <div class="body"> <a href="{{ child.url }}" class="{{ child.class}}">{{ child.title }}</a> </div> {% endfor %} </div> </div> {% endfor %} </div>
4、按钮权限分配
用户有没有这个按钮就看看他有没有这个按钮的权限
通过过滤器判断请求的url值否在权限列表里
{% load mytag %} {% if "/add_customers/"|haspermission:request %} <a href="{% url "add_customers" %}" class="btn btn-primary pull-right">添加</a> {% endif %}
@register.filter # 分配按钮权限 def haspermission(base_url,request): for item in request.session["permisson_list"]: ret = "^%s$"%item["url"] res = re.search(ret,base_url) if res: return True return False
通过过滤器判断用户权限的url中有没有该按钮的url,有就说明该用户有这个按钮的权限,让其显示;反之隐藏
{% if "/edit_customers/1/"|haspermission:request or "/delete_customers/1/"|haspermission:request %} <td> {% if "/edit_customers/1/"|haspermission:request %} <a href="{% url "edit_customers" customers.pk %}" class="btn btn-primary btn-sm">编辑</a> {% endif %} {% if "/delete_customers/1/"|haspermission:request %} <a href="{% url "delete_customers" customers.pk %}" class="btn btn-danger btn-sm">删除</a> {% endif %} </td> {% endif %}
5、路径导航
页面效果:
点击我的客户页面中的其他功能:基于我的客户后面添加
1、首页是默认的,无论点击哪个菜单路径导航第一个都是首页; 2、点击左侧菜单路径导航显示对应的名称, 3、点击左侧菜单栏页面中对应的功能时路径导航基于菜单栏的路径继续往后添加;
实现思路:
我们只需要判断请求的路径是不是二级菜单: 如果是二级菜单取出对应的路径title和url在前端直接渲染即可; 如果不是二级菜单那么就是二级菜单的功能请求,我们取出功能请求的title和url并需要根据已知条件查出它父级菜单的title和url,在前端渲染时放在自己的前面;
代码:
每次请求都会走一遍中间件,权限认证通过之后判断是二级菜单还是二级菜单中的功能请求;
用bootstrap给最后一层添加灰色样式
<ol class="breadcrumb"> {% for foo in request.path_navigation %} {% if forloop.last %} <li class="active">{{ foo.title }}</li> {% else %} <li><a href="{{ foo.url }}">{{ foo.title }}</a></li> {% endif %} {% endfor %} </ol>