深度權限管理系統
需求:
最終保證,每個人有不同的權限,而權限的分配主要是通過url來判定
先來看models表的演變過程,以及需求的數據演變過程
2 張表
用戶表 權限表 一對多關系
joker /user/
/user/add ...
那如果這樣,新來名員工就會配置N個權限,操作復雜?如果加入角色?
3 張表 外加 1張 多對多表 用戶表 多對多 角色 一對多 權限表 joker 經理 /user/... ceo /all/...
permiss_list=user.roles.filter(permissions__url__isnull=False).value('permissions__title','permissions__url').distinct() # request.session['is_login'] = True request.session['user'] = user.username # print(user.username) url_list = [] for url in permiss_list: print(url) url_list.append(url['permissions__url']) request.session[settings.PERMISSION_SESSION_KEY] = url_list 添加到配置文件中去,在判斷用戶每次的輸入url是否在這里
如果這樣,新來的員工直接分配角色就可以了,對?沒錯這樣基本實現了權限分配,但是對於我們開發前端的話,我們必須直到用戶進來有多少權限,才能針對頁面做按鈕級別的控制,因為不能用戶進來所有的增刪改查按鈕都在?那如果加入權限組?
4 張表 外加 2 張多對多表 用戶表 多對多 角色 一對多 權限表(增加code字段,對url的解釋,例如/user/,code為list) 多對一 權限組 joker 經理 /user/... 1 ceo /all/.... 1 注意去重
想要得到的數據效果,該用戶下的權限組ID下的所有權限 1 {'urls': ['/userinfo/', '/userinfo/del/(\\d+)', '/userinfo/edit/(\\d+)', '/userinfo/add/'], 'codes': ['list', 'del', 'edit', 'add']} 2 {'urls': ['/order/', '/order/del/(\\d+)', '/order/add/', '/order/edit/(\\d+)'], 'codes': ['list', 'del', 'add', 'edit']} permiss_list = user.roles.all().values('permissions__code','permissions__group_id','permissions__url').distinct() # permiss_dict = {} for item in permiss_list: codes = item['permissions__code'] group_id = item['permissions__group_id'] urls = item['permissions__url'] if permiss_dict.get(group_id): permiss_dict[group_id]['codes'].append(codes) permiss_dict[group_id]['urls'].append(urls) else: permiss_dict[group_id]={ 'codes':[codes,], 'urls':[urls,] } for k,v in permiss_dict.items(): print(k,v) request.session['is_login'] = True request.session['user'] = user.username # user 傳過來的是一個對象 request.session[settings.PERMISSION_SESSION_KEY] =permiss_dict 將權限列表加入到settings里面去,在根據用戶輸入的url來判斷有什么權限
這樣完全實現了權限分配,那我們將這個權限控制放在中間鍵吧?這樣可以每次只要請求過來就可以進行判定,很方便
from django.utils.deprecation import MiddlewareMixin import re from django.shortcuts import HttpResponse from django.conf import settings class RbacMiddleware(MiddlewareMixin): def process_request(self, request): # 1. 獲取白名單,讓白名單中的所有url和當前訪問url匹配 for reg in settings.PERMISSION_VALID_URL: if re.match(reg, request.path_info): return None # 2. 獲取權限 premiss_list = request.session.get(settings.PERMISSION_SESSION_KEY) if not url_list: return HttpResponse('未獲取到當前用戶的權限信息,無法訪問') # 3. 對用戶請求的url進行匹配 flag = False for reg in url_list: # 需要注意 regx = "^%s$" % (reg,) if re.match(regx, request.path_info): print('====') flag = True # break return None if not flag: return HttpResponse('無權訪問')
我們看到中間鍵里面有一些針對settings的配置我們看下配置了哪些?
1. settings導入方法是 from django.conf import settings 2. 中間鍵的注冊 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.middlewares.rbac.RbacMiddleware' 應用.目錄.目錄.類名 過濾url驗證 ] 3. 記得在數據機構化的時候,我們把數據加到了settings里面,所以要在里面加入 PERMISSION_SESSION_KEY = "url_listt" # 權限字典,包含列表
MENU_SESSION_KEY = 'menu_list' # 后面的菜單設計
4. 中間鍵過濾的url,我們發現一個問題,如果登陸都不再權限內,那將會無限死循環,所以我們需要一個url白名單 PERMISSION_VALID_URL = [ # 白名單 '/login/', '/admin/.*', '/index/', '/menu/', ] 這里面的url都是可以不用進行url驗證
當我們url驗證通過,拿到用戶下的權限組下的所有權限,那我們如何處理這些權限呢,從而實現在前端可以針對性的控制按鈕?
還記得我們code么,url的別名?我們只需要判斷用戶輸入的url是否有通過驗證,通過就會有對應的code?
class PermissCode: def __init__(self, premiss_codes_list): self.premiss_codes_list = premiss_codes_list def has_add(self): if 'add' in self.premiss_codes_list: return True def has_del(self): if 'del' in self.premiss_codes_list: return True def has_edit(self): if 'edit' in self.premiss_codes_list: return True def orderinfo(request): premiss_codes_list = request.premiss_codes_list # 拿到 code列表 page_permission = PermissCode(premiss_codes_list) # 實例化 return render(request,'order.html',{'page_permission':page_permission}) data_list是數據,只需要觀察如何顯示權限的就可以了 {% block content %} {% if page_permission.has_add %} <a href="/userinfo/add/">添加</a> {% endif %} <table> {% for row in data_list %} <tr> <td>{{ row.id }}</td> <td>{{ row.name }}</td> {% if page_permission.has_edit %} <td><a href="#">編輯</a></td> {% endif %} {% if page_permission.has_del %} <td><a href="#">刪除</a></td> {% endif %} </tr> {% endfor %} </table> {% endblock %}
html用到了模版,因為頁面都一樣,這樣可以更加的彈性,伸縮
上面數據展示的代碼有rbac應用,server目錄下的init_permission初始化獲取,視圖里面調用這個函數就可以
加入菜單管理
菜單設計顧名思義,就是當我選中一個左側菜單時候,顯示該菜單下的權限,其他菜單下的權限要閉合?
那我們肯定是要增加表,那么菜單表要跟誰對應呢,應該是權限組,你要想,我們上面數據最終得到的是權限組=[url:{},code:{}],那我們將菜單組跟權限組關聯,是不是就可以替代權限組的位置,而且在前端顯示的時候,菜單下對應的權限也會多,也很美觀。
5 張表 外加 2 張多對多表 用戶表 多對多 角色 一對多 權限表(增加code字段,對url的解釋,例如/user/,code為list) 多對一 權限組 多對一 菜單 joker 經理 /user/... 1 菜單一 1 ceo /all/... 1 菜單二 1 注意去重
我們首先從數據庫中拿到這樣的數據放入到settings中,我們有專門的函數來處理。
# user 是個對象
menu_list = user.roles.all().values('permissions__id', # 權限ID 'permissions__url', # 權限 url 'permissions__code', # 權限 code 別名 'permissions__title', # 權限名稱 'permissions__is_menu', # 是否是菜單 'permissions__group_id', # 權限組ID 'permissions__parent_id', # 自關聯ID 'permissions__group__menu__id', # 菜單ID 'permissions__group__menu__title').distinct() # 菜單名稱 print(menu_list) # 用於生成菜單 menu_list_all = []
for item in menu_list: tpl = { 'id': item['permissions__id'], 'title': item['permissions__title'], 'url': item['permissions__url'], 'menu_gp_id': item['permissions__parent_id'], 'menu_id': item['permissions__group__menu__id'], 'menu_title': item['permissions__group__menu__title'], } menu_list_all.append(tpl) request.session[settings.MENU_SESSION_KEY] = menu_list_all ######### 菜單 print(menu_list_all)
{'title': '用戶列表', 'url': '/userinfo/', 'pid': None, 'id': 1, 'menu_id': 1, 'menu_title': '菜單一'},
{'title': '用戶刪除', 'url': '/userinfo/del/(\\d+)', 'pid': None, 'id': 2, 'menu_id': 1, 'menu_title': '菜單一'},
{'title': '訂單列表', 'url': '/order/', 'pid': None, 'id': 3, 'menu_id': 2, 'menu_title': '菜單二'},
{'title': '訂單刪除', 'url': '/order/del/(\\d+)', 'pid': None, 'id': 4, 'menu_id': 2, 'menu_title': '菜單二'},
{'title': '用戶修改', 'url': '/userinfo/edit/(\\d+)', 'pid': None, 'id': 5, 'menu_id': 1, 'menu_title': '菜單一'},
{'title': '用戶添加', 'url': '/userinfo/add/', 'pid': None, 'id': 6, 'menu_id': 1, 'menu_title': '菜單一'},
{'title': '訂單添加', 'url': '/order/add/', 'pid': None, 'id': 7, 'menu_id': 2, 'menu_title': '菜單二'},
{'title': '訂單修改', 'url': '/order/edit/(\\d+)', 'pid': None, 'id': 8, 'menu_id': 2, 'menu_title': '菜單二'}
我們在將上面得到數據進行數據結構化?
菜單一:{ 1: { 'active': True, # 是否展開
'menu_id': 1, 'children': [{'url': '/userinfo/', 'active': True, 'title': '用戶列表'}, {'url': '/order/', 'active': None, 'title': '訂單列表'}], 'menu_title': '菜單一'} } } '''
如果得到上面的結構,我們就可以通過active的值來判定誰展開,誰關閉。children代表了用戶權限,通過active來判定是否選中,是通過用戶的url來判定。
並且通過孩子的active屬性來判定父的active屬性值,默認為None,但是當孩子的active為True,父就會隨之變為True,展開。
我們知道為何要這種數據結構了,還有個問題?如果視圖非常多(也肯定非常多,比如用戶列表,訂單列表,還要增刪改查),我們不能在視圖里面重復過多的寫入這種數據結構的演變,我們可以寫入一個單獨文件內,這樣去調用就可以了,還寂寞模版么?所有的html都是基於它展現出來,而結構出來的菜單設計也是需要跟模版進行渲染,所以,我們把結構化數據,和一部分模版上的左邊菜單進行渲染,利用到了register.inclusion_tag方法?
應用下面創建 templatetags目錄,在創建rbac.py文件
from django.template import Library register = Library() # 實例化 @register.simple_tag def menu_html(): return '菜單' # 返回什么就是什么 from django.conf import settings import re @register.inclusion_tag('xxxxx.html') def menu_html_new(request): menu_list = request.session.get(settings.MENU_SESSION_KEY) current_url = request.path_info # 請求的URL menu_dict = {} for item in menu_list: if not item['menu_gp_id']: menu_dict[item['id']] = item print(menu_dict) ''' {1: {'id': 1, 'menu_id': 1, 'url': '/userinfo/', 'menu_title': '菜單一', 'menu_gp_id': None, 'title': '用戶列表'}, 3: {'id': 3, 'menu_id': 1, 'url': '/order/', 'menu_title': '菜單一', 'menu_gp_id': None, 'title': '訂單列表'}} ''' for item in menu_list: regex = "^{0}$".format(item['url']) if re.match(regex, current_url): menu_gp_id = item['menu_gp_id'] if menu_gp_id: menu_dict[menu_gp_id]['active'] = True else: menu_dict[item['id']]['active'] = True print('=======') print(menu_dict) ''' { { 1: {'active': True, 'id': 1, 'menu_id': 1, 'url': '/userinfo/', 'menu_title': '菜單一', 'menu_gp_id': None, 'title': '用戶列表'}, # 默認展開 3: {'id': 3, 'menu_id': 1, 'url': '/order/', 'menu_title': '菜單一', 'menu_gp_id': None, 'title': '訂單列表'}} # 不展開 } ''' result = {} for item in menu_dict.values(): active = item.get('active') menu_id = item['menu_id'] if menu_id in result: result[menu_id]['children'].append({'title': item['title'], 'url': item['url'], 'active': active}) if active: result[menu_id]['active'] = True else: result[menu_id] = { 'menu_id': item['menu_id'], 'menu_title': item['menu_title'], 'active': active, 'children': [ {'title': item['title'], 'url': item['url'], 'active': active} ] } print(result) ''' ## 菜單 菜單一:{ 1: { 'active': True, 'menu_id': 1, 'children': [{'url': '/userinfo/', 'active': True, 'title': '用戶列表'}, {'url': '/order/', 'active': None, 'title': '訂單列表'}], 'menu_title': '菜單一'} } } ''' return {'menu_dict':result}
里面的xxxx.html是判定左邊菜單的展示還是收縮,還判定權限是否被選中
{% for value in menu_dict.values %}
<div class="item-title">
<div class="header item">{{ value.menu_title }}</div>
{% if value.active %}
<div class="body">
{% else %}
<div class="body hide">
{% endif %}
{% for child in value.children %}
{% if child.active %}
<a href="{{ child.url }}" class="active">{{ child.title }}</a>
{% else %}
<a href="{{ child.url }}">{{ child.title }}</a>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
將上面結構數據,和html渲染過后的數據放回模版里面 dynamic_menu.html
{% load rbac %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<script src="https://cdn.bootcss.com/jquery/3.1.1/jquery.js"></script>
<link rel="stylesheet" href="/static/rbac/rbac.css">
</head>
<body>
<div class="pg-header">表頭</div>
<div class="context">
<div style="width: 20%;float: left;background-color: cadetblue">
{# 這里代碼移交至 #}
{% menu_html_new request%}
</div>
<div style="width: 80%;float: left">
{% block content %}
{% endblock %}
<script src="/static/rbac/rbac.js"></script>
</div>
</div>
</body>
</html>
其他的html文件只需要繼承就可以了,在block里面寫入當前頁面需要的內容
例如用戶添加頁面
{% extends "dynamic_menu.html" %}
{% block content %}
<h1>添加用戶頁面</h1>
<input type="text">
{% endblock %}
最后你需要注意的是,settings文件配置了static靜態文件,你可以將一些css,js的引用放到這里,收縮你的代碼
項目加入權限系統的過程
1. rbac清空migrations目錄(除__init__.py以外) 2. 業務的用戶表和權限的用戶表OneToOne關聯,如: from django.db import models from rbac import models as rbac_model class DepartMent(models.Model): """ 部門 """ title = models.CharField(max_length=32) class User(models.Model): user_info = models.OneToOneField(to=rbac_model.UserInfo) nickname = models.CharField(max_length=32) momo = models.CharField(max_length=32) gender_choices = ( (1,'男'), (2,'女'), ) gender = models.IntegerField(choices=gender_choices) 3. 通過頁面admin錄入權限信息 4. 用戶登錄成功之后,初始化權限和菜單信息 init_permission(權限的用戶表對象,request) 加入權限相關的配置: PERMISSION_SESSION_KEY = “url_list” MENU_SESSION_KEY = “menu_list” 5. 對用戶請求的url進行權限的驗證 應用中間件: MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', ... 'rbac.middlewares.rbac.RbacMiddleware', #中間鍵對於url的判定 ] 中間鍵里面有白名單, setting.py PERMISSION_VALID_URL = [ '/login/', '/admin/.*', ] 有針對數據權限的判定 PERMISSION_SESSION_KEY 只要通過驗證,在視圖函數的request中 permission_codes字段: [list,add,edit,del....] 如果想要面向對象方式, from rbac.permission.base import BasePermission class PermissCode: def __init__(self, premiss_codes_list): self.premiss_codes_list = premiss_codes_list def has_add(self): if 'add' in self.premiss_codes_list: return True 給前端傳過實例之后,就可以判斷有沒有這個屬性 判斷url所有權限,通過code判定,然后可以針對按鈕級別是否顯示 6. 動態菜單 在Html模板中引入: {% load rbac %} 引入樣式 <link rel='stylesheet' href='/static/rbac/rbac.css' /> <script src='/static/rbac/rbac.js' /> {% menu request %} 推薦:放到母板中。
