权限管理rbac:基于角色的权限控制
权限管理rbac:基于角色的权限控制
table相关
table的设计
1 from django.db import models 2 3 class UserInfo(models.Model): 4 username = models.CharField(max_length=40) 5 password = models.CharField(max_length=40) 6 email = models.EmailField(null=True, blank=True) 7 nickname = models.CharField(max_length=40, null=True, blank=True) 8 role = models.ManyToManyField("Role") 9 10 def __str__(self): 11 return self.username 12 13 14 class Role(models.Model): 15 name = models.CharField(max_length=32) 16 authority = models.ManyToManyField("Authority") 17 18 def __str__(self): 19 return self.name 20 21 22 class Authority(models.Model): 23 title = models.CharField(max_length=32) 24 url = models.CharField(max_length=100) 25 menu = models.ForeignKey("Menu", null=True, blank=True) 26 27 def __str__(self): 28 return self.title 29 30 31 class Menu(models.Model): 32 title = models.CharField(max_length=32) 33 pid = models.ForeignKey("self", null=True, blank=True) 34 35 def __str__(self): 36 menu_title_list = [self.title] 37 pid = self.pid 38 while pid: 39 menu_title_list.insert(0, pid.title) 40 pid = pid.pid 41 return "-".join(menu_title_list)
主要使用以下几张表:
- UserInfo
- 用户名/密码/邮箱等基本信息
- 与 Role 多对多的关系
- Role
- 角色的名字(相当于用户组)
- 与 Authority 多对多的关系
- Authority
- 权限的名称 -- 用于显示
- 权限的路径 -- 用于访问
- 创建 Menu 的外键
- Menu
- 菜单的名称
- 菜单的父级 id
- role_authority
- 由多对多创建,存放 Role 的id, Authority 的id
- userinfo_role
- 由多对多创建, 存放Userinfo 的id, Role 的id
table注册
导入 admin 模块以及存放定义了表格的 类,通过 admin.site.register( )注册
1 from django.contrib import admin 2 from . import models 3 4 admin.site.register(models.UserInfo) 5 admin.site.register(models.Role) 6 admin.site.register(models.Authority) 7 admin.site.register(models.Menu)
登陆后的设置
在登陆成功以后,该用户的权限路径/菜单等存入 request.session 中
1 from .. import models 2 from django.conf import settings # django 中有默认的配置以及自定义的配置,会在这里拿到合并后的结果 3 4 def login_init(request, user_obj): # 由于需要使用到 session 以及 登陆的对象,所以需要调用者传入 5 authority_list = user_obj.role.values("authority__title", 6 "authority__url", 7 "authority__menu_id").distinct() 8 url_list = [] 9 url_menu_list = [] 10 for item in authority_list: 11 url_list.append(item["authority__url"]) # 将所有的权限 url 放入到 url_list,最后存入到 session 中; 12 if item["authority__menu_id"]: # 将拥有 菜单id 的权限的这条记录放入到 url_menu_list,最后存入到 session 中; 13 url_menu_list.append({ 14 "title": item["authority__title"], 15 "url": item["authority__url"], 16 "menu_id": item["authority__menu_id"], 17 }) 18 menu_list = list(models.Menu.objects.values("id", "title", "pid")) # 因为菜单相对的会比较小,所以将所有的菜单信息加入到 menu_list 中,最后存入到 session 中; 19 20 request.session[settings.PERMISSION.get("session_persission_list")] = url_list 21 request.session[settings.PERMISSION.get("session_persission_menu_list")] = { 22 "url_menu_list": url_menu_list, 23 "menu_list": menu_list, 24 }
中间件设置
在实际过程中,如果该用户访问的 url 不在自己的权限之内,那么应该直接返回,不需要进入到试图函数等流程中,所以,我们可以在中间件中的 request 中进行判断,由于需要使用到 session,所以,自定义的中间件应该放在最后的位置
1 MIDDLEWARE = [ 2 'django.middleware.security.SecurityMiddleware', 3 'django.contrib.sessions.middleware.SessionMiddleware', 4 'django.middleware.common.CommonMiddleware', 5 'django.middleware.csrf.CsrfViewMiddleware', 6 'django.contrib.auth.middleware.AuthenticationMiddleware', 7 'django.contrib.messages.middleware.MessageMiddleware', 8 'django.middleware.clickjacking.XFrameOptionsMiddleware', 9 'rbac.middleware.filterPermission.FilterPermission', # 自定义的中间件 10 ]
中间件注册时,自定义的 class 应该继承与 MiddlewareMixin,导入方式为:from django.utils.deprecation import MiddlewareMixin 如果它不存在了,可以手写一个,然后继承,代码如下:
1 class MiddlewareMixin(object): 2 def __init__(self, get_response=None): 3 self.get_response = get_response 4 super(MiddlewareMixin, self).__init__() 5 6 def __call__(self, request): 7 response = None 8 if hasattr(self, 'process_request'): 9 response = self.process_request(request) 10 if not response: 11 response = self.get_response(request) 12 if hasattr(self, 'process_response'): 13 response = self.process_response(request, response) 14 return response
继承完成后,便可以进行自定义的中间件插件的编写了
1 import re 2 from django.utils.deprecation import MiddlewareMixin 3 from django.conf import settings 4 from django.shortcuts import redirect 5 6 7 class FilterPermission(MiddlewareMixin): 8 """ 9 中间件,用来过滤权限,在 10 setting 中设置 11 rule:规定路径为开头结尾,用来拼接 url,格式为: "^{0}$" 12 session_persission_list: 存放 session 中权限路径的 key 13 safe_url:存放安全路径,登录前后都可以进入的列表,如:登录页面,注册页面等 14 """ 15 def process_request(self, request): 16 permission_url = request.session.get(settings.PERMISSION.get("session_persission_list")) 17 current_url = request.path_info 18 rule = settings.PERMISSION.get("rule") 19 20 for url in settings.PERMISSION.get("safe_url"): 21 if re.match(rule.format(url), current_url): 22 return None 23 24 for url in permission_url: 25 if re.match(rule.format(url), current_url): 26 return None 27 28 return redirect("/login.html")
这样一来,我们便可以自动过滤/拦截没有权限的访问了
自定义过滤器
上面已经完成了中间件拦截非法路径的问题,下面我们需要对我们写的权限管理系统中的数据进行管理,那么,我们可以在页面做出一个菜单进行管理(多级菜单),这里我们通过 自定义过滤器 的方式实现
效果图,很粗糙

过滤器定义
1. 自定义的过滤器放到 templatetags 目录下
2. 引入 from django.template import Library
3. 实例化 register = Library( )
4. 在函数上加装饰器 (这里只用到了 simple_tag 一种) @register.simple_tag
1 import re 2 import os 3 from django.conf import settings 4 from django.template import Library 5 from django.utils.safestring import mark_safe 6 7 register = Library() 8 9 10 11 12 def get_menus_dict(request): 13 url_menu_list = request.session[settings.PERMISSION["session_persission_menu_list"]]["url_menu_list"] 14 menu_list = request.session[settings.PERMISSION["session_persission_menu_list"]]["menu_list"] 15 16 ret = [] 17 menu_dict = {} 18 current_path = request.path_info 19 # url_menu_list = [{'menu_id': 4, 'url': '/rbac/user/', 'title': '用户管理'}] 20 # menu_list = [{'id': 1, 'title': '用户管理', 'pid': None}] 21 for item in menu_list: 22 item["children"] = [] 23 item["open"] = False 24 item["status"] = False 25 menu_dict[item["id"]] = item 26 27 for item in url_menu_list: 28 flag = False 29 30 pid = item["menu_id"] 31 menu_dict[pid]["children"].append(item) 32 if re.match(settings.PERMISSION['rule'].format(item["url"]), current_path): 33 flag = True 34 item["status"] = True 35 else: 36 item["status"] = False 37 38 while pid: 39 menu_dict[pid]["status"] = True 40 if flag: 41 menu_dict[pid]["open"] = True 42 pid = menu_dict[pid]["pid"] 43 44 for k, v in menu_dict.items(): 45 # {'status': True, 'children': [], 'id': 1, 'title': '用户管理', 'pid': None, 'open': False} 46 if not v["pid"]: 47 ret.append(v) 48 else: 49 menu_dict[v["pid"]]["children"].append(v) 50 51 return ret 52 53 54 def get_menus_html(menu_list): 55 tpl1 = """ 56 <div class='rbac-menu-item'> 57 <div class='rbac-menu-header'>{0}</div> 58 <div class='rbac-menu-body {2}'>{1}</div> 59 </div> 60 """ # 父级别标签, {0} 菜单名字 {1} 子菜单信息 {2} class名称,用来确当是否展开这个菜单 61 62 63 tpl2 = """ 64 <a href='{0}' class='{1}'>{2}</a> 65 """ # 权限标签 {0} 权限路径 {2} 权限名称 66 67 html = '' 68 for item in menu_list: 69 if item.get('url', None): 70 html += tpl2.format(item['url'], 'rbac_active' if item.get('status') else '', item.get('title')) 71 elif not item.get('status'): 72 continue 73 else: 74 html += tpl1.format(item['title'], get_menus_html(item['children']), '' if item.get('open') else 'rbac-hide') 75 76 return html 77 78 79 @register.simple_tag 80 def rbac_menus(request): 81 # 将 request.session 中的数据取出来处理成列表中包含字典的形式 82 # 用作构建 标签 发送给前端 83 menu_dict = get_menus_dict(request) 84 # 用上面的数据,用来构建标签 85 html = get_menus_html(menu_dict) 86 87 88 89 return mark_safe(html) 90 91 92 @register.simple_tag 93 def rbac_css(): # 读取css代码,放入到前端中 94 file_path = os.path.join('rbac', 'theme', 'rbac.css') 95 if os.path.exists(file_path): 96 return mark_safe(open(file_path, 'r', encoding='utf-8').read()) 97 else: 98 raise Exception('rbac主题CSS文件不存在') 99 100 101 @register.simple_tag 102 def rbac_js(): # 读取js代码,放入到前端中 103 file_path = os.path.join('rbac', 'theme', 'rbac.js') 104 if os.path.exists(file_path): 105 return mark_safe(open(file_path, 'r', encoding='utf-8').read()) 106 else: 107 raise Exception('rbac主题JavaScript文件不存在')
拼接后的字符串放入到前端以后,需要使用 safe 过滤器才能进行渲染,但是在自定义过滤器这里,似乎不太方便,这里使用一个新的方法
1. 引入 from django.utils.safestring import mark_safe
2. 使用 mark_safe(html)
from django.utils.safestring import mark_safe mark_safe(html)
前端使用
下面是使用上面过滤器的页面
1 {% load rbac %} 2 {% load static %} 3 4 <!DOCTYPE html> 5 <html lang="en"> 6 <head> 7 <meta charset="UTF-8"> 8 <meta http-equiv="x-ua-compatible" content="IE=edge"> 9 <meta name="viewport" content="width=device-width, initial-scale=1"> 10 <title>Title</title> 11 <style> 12 .head{ 13 height: 44px; 14 background-color: #369; 15 } 16 17 .left{ 18 float:left; 19 width: 200px; 20 background-color: azure; 21 } 22 23 .right{ 24 margin-left:200px; 25 background-color: beige; 26 } 27 </style> 28 <style> 29 {% rbac_css %} 30 </style> 31 <script src="{% static 'rbac/js/jquery-1.12.4.js' %}"></script> 32 </head> 33 <body> 34 <div class="head"></div> 35 <div class="body"> 36 <div class="left"> 37 {% rbac_menus request %} 38 </div> 39 <div class="right"></div> 40 </div> 41 <div class="foot"></div> 42 </body> 43 <script> 44 {% rbac_js %} 45 </script> 46 </html>
由于我在设置编辑的时候,左面的菜单是固定不变的,所以这里用到了一个 母版的继承
1. 定义 母版 的时候,需要自定义的位置使用 {% block 名字%}{% endblock %} 来定义
2. 使用的时候,在 html 文件的开头使用 {% extends "母版路径" %} 来申明,使用时重写 {% block 名字%}{% endblock %} 里面的内容
这里列出一个 人物信息 的前段代码来做例子
1 {% extends "rbac/base.html" %} 2 3 {% block head %} 4 <style> 5 .clearfix:after,.clearfix:before{ 6 content: ""; 7 display:block; 8 } 9 .clearfix:after{ 10 clear:both; 11 } 12 .clearfix{ 13 zoom:1; 14 } 15 16 .fl{ 17 float: left; 18 } 19 20 .fr{ 21 float: right; 22 } 23 ul li{ 24 float: left; 25 list-style: none; 26 text-indent: 0; 27 } 28 .id{ 29 width:8% 30 } 31 .username, .password, .email, .nickname{ 32 width: 20%; 33 } 34 .button{ 35 width:12%; 36 } 37 .body ul{ 38 {# padding: 20px 0;#} 39 height:20px; 40 border-bottom: 1px solid #ddd; 41 } 42 {# .add{#} 43 {# position: relative;#} 44 {# right:-100px;#} 45 {# top:0;#} 46 {# }#} 47 48 </style> 49 {% endblock %} 50 51 {% block body %} 52 <div class="clearfix body"> 53 <a href="/rbac/user/add/" class="add"><input type="button" value="添加"></a> 54 <ul> 55 <li class="id">id</li> 56 <li class="username">username</li> 57 <li class="password">password</li> 58 <li class="email">email</li> 59 <li class="nickname">nickname</li> 60 <li class="button">编辑</li> 61 </ul> 62 {% for user_obj in user_obj_list %} 63 <ul> 64 <li class="id">{{ user_obj.id }}</li> 65 <li class="username">{{ user_obj.username }}</li> 66 <li class="password">{{ user_obj.password }}</li> 67 <li class="email">{{ user_obj.email }}</li> 68 <li class="nickname">{{ user_obj.nickname }}</li> 69 <li class="button"> 70 <a href="/rbac/user/edit/{{ user_obj.id }}/" class="edit"><input type="button" value="修改"></a> 71 <a href="/rbac/user/delete/{{ user_obj.id }}/" class="delete"><input type="button" value="删除"></a> 72 </li> 73 </ul> 74 {% endfor %} 75 </div> 76 77 {% endblock %}
下面是我这个目录的结构

相关的配置文件
STATIC_URL = '/static/'
PERMISSION = {
"rule": "^{0}$",
"session_persission_list": "session_persission_list",
"session_persission_menu_list": "session_persission_menu_list",
"safe_url": ["/login.html", "/register.html", "/admin/.*", "/rbac/.*"],
}
ModelForm
浙公网安备 33010602011771号