RABC許可權控制(二級選單實現)
目前大部分系統由於使用者體驗,基本上選單不會做的很深,以二級選單為例,做了一個簡單的許可權控制實現,可精確到按鈕級別(基於django),下面具體看看實現
1.表結構的設計
無論開發什麼都需要先梳理清楚需求,然後再考慮表結構,這裡先來說說大致的表結構組成,注意,我的許可權控制是通過url做的,所以控制的核心就在於控制url
表字段介紹設計如下:
許可權表 url # 許可權 title #許可權的標題,左側展示,代表的功能(因為不可能展示url吧) menu # 所屬的一級選單,外來鍵關聯一級選單 parent # 二級選單下的子許可權,類似xx列表,旗下的增刪改就是子許可權,所以這個需要外來鍵自關聯當前表 url_name # url分發的別名,主要是用於按鈕級別許可權控制,也是為了之後的擴充套件 icon # 二級選單的圖示 角色表 name # 角色的名稱 permissions # 與許可權表多對多的關係,一個角色可以有多個許可權,一個許可權也可以給多個角色 使用者表 name # 使用者名稱 pwd # 加密後的密碼 roles # 與角色表是多對多的關係 一級選單表 title # 一級選單的標題 icon # 一級選單的圖示 weight # 一級選單的權重,通過權重控制一級選單的順序,權重最大在最上面 整體邏輯就是創了使用者後,可以給該使用者分配角色,由於角色擁有特定許可權,所以使用者久擁有了相應的許可權
程式碼如下:
from django.db import models class Menu(models.Model): """ 一級選單表 """ title = models.CharField(max_length=32, verbose_name='一級選單') icon = models.CharField(max_length=32, verbose_name='圖示', null=True, blank=True) weight = models.IntegerField(verbose_name='選單權重', default=1) 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='許可權') icon = models.CharField(max_length=32, verbose_name='圖示', null=True, blank=True) menu = models.ForeignKey(to='Menu', verbose_name='所屬選單', on_delete=models.CASCADE, null=True, blank=True) url_name = models.CharField(max_length=32, verbose_name='url別名', null=True, blank=True) parent = models.ForeignKey('self', on_delete=models.CASCADE, verbose_name='父級選單', null=True, blank=True) # is_menu = models.BooleanField(default=False, verbose_name='是否是選單') 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 class User(models.Model): """ 使用者表 """ name = models.CharField(max_length=32, verbose_name='使用者名稱') pwd = models.CharField(max_length=32, verbose_name='密碼') roles = models.ManyToManyField(to='Role', verbose_name='使用者所擁有的角色', blank=True) def __str__(self): return self.name
四個模型,6張表,因為使用者和角色,角色和許可權都是多對多的關係,所以django會自動生成兩張表記錄多對多的關係.
2.成功登入後初始化使用者資訊(合適的資料結構的設計非常重要也比較難)
將表資料錄入後,就表示使用者擁有了不同許可權,因此我們以使用者身份去登入平臺,首先登陸平臺需要登入,因此在中介軟體中需要先設定白名單,避免登入,註冊等url也被許可權控制攔截,登入認證成功後,就將相關資訊存入使用者的session中,這裡來詳細說明下存了哪些資訊,直接看程式碼,我在程式碼裡面進行步驟和備註說明
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author: Xiaobai Lei from rbac.models import Role def initial_session(user_obj, request): """ 將當前登入人的資訊記錄到session中 :param user_obj: 使用者物件 :param request: :return: """ # 1.查詢當前登入人的許可權列表,取出相關的資訊 permissions = Role.objects.filter(user=user_obj).values('permissions__url', 'permissions__pk', 'permissions__title', 'permissions__icon', 'permissions__parent_id', 'permissions__url_name', 'permissions__menu__pk', 'permissions__menu__title', 'permissions__menu__weight', 'permissions__menu__icon').distinct() # 2.儲存登入人的相關資訊(許可權列表,別名列表和許可權選單字典) """ 許可權列表,以字典形式儲存當前使用者的每一個許可權資訊,資料格式如下: permission_list = [ {'id': 1, 'url': '/custmer/list/', 'title': '客戶列表', 'parent_id': None}, {'id': 2, 'url': '/custmer/add/', 'title': '客戶新增', 'parent_id': 1}, ] 原先簡單設計只是列表儲存當前使用者的所有url,後面發現訪問子許可權(比如客戶新增)時,依舊需要左側客戶列表展示, 所以需要用到父許可權(客戶列表)的資訊,而且為了更多擴充套件,所以採用了列表巢狀字典的形式儲存了較多資料 """ # 許可權列表,主要用於使用者的許可權校驗 permission_list = [] # 別名列表,主要用於按鈕級別的控制,比如客戶新增的按鈕 permission_url_names = [] """ 許可權選單字典,資料格式如下: permission_menu_dict = { '一級選單id': { 'menu_title': '資訊管理', 'menu_icon': '一級選單圖示', 'menu_weight': '一級選單的權重', 'menu_children': [ {'id': 1, 'url': '/custmer/list/', 'title': '客戶列表', 'parent_id': None}, ] } } 注意:menu_chidren只儲存的是二級選單(如客戶列表),通過這個資料結構就可以很清晰的看到層級關係了,如果還有一級選單 的話,那麼就需要在客戶列表字典結構中再加入一個node_children:[{}],就是一個不斷迴圈巢狀的過程,你懂的 """ # 許可權選單字典,主要用於左側選單的資料展示 permission_menu_dict = {} # 迴圈獲取上面提及的資料結構 for item in permissions: permission_list.append({ 'url': item['permissions__url'], 'id': item['permissions__pk'], 'parent_id': item['permissions__parent_id'], 'title': item['permissions__title'], }) permission_url_names.append(item['permissions__url_name']) menu_id = item['permissions__menu__pk'] # 只有二級選單才被加入,也就是父許可權(如客戶列表) if menu_id: # 如果字典中已經存在了選單id就直接在一級選單的menu_chidren下追加,沒有則先新建 if menu_id not in permission_menu_dict: permission_menu_dict[menu_id] = { 'menu_title': item['permissions__menu__title'], 'menu_icon': item['permissions__menu__icon'], 'menu_weight': item['permissions__menu__weight'], 'menu_children': [ { 'title': item['permissions__title'], 'url': item['permissions__url'], 'icon': item['permissions__icon'], 'id': item['permissions__pk'], }, ] } else: permission_menu_dict[menu_id]['menu_children'].append({ 'title': item['permissions__title'], 'url': item['permissions__url'], 'icon': item['permissions__icon'], 'id': item['permissions__pk'], }) # 根據一級選單權重進行重新排序 permission_menu_dict_new = {} for i in sorted(permission_menu_dict, key=lambda x: permission_menu_dict[x]['menu_weight'], reverse=True): permission_menu_dict_new[i] = permission_menu_dict[i] # 將使用者的許可權列表和許可權選單列表注入session中 request.session['permission_list'] = permission_list request.session['permission_url_names'] = permission_url_names request.session['permission_menu_dict'] = permission_menu_dict_new
3.許可權校驗(採用django自定義中介軟體)
由於每次訪問都是需要進行許可權校驗的,因此就放在了中介軟體中,之前也提到過,在許可權校驗之前你必須是登入成功的使用者,因此中介軟體中還加入了使用者認證,具體請見如下:
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author: Xiaobai Lei import re from django.utils.deprecation import MiddlewareMixin from django.shortcuts import ( redirect, reverse, HttpResponse ) from rbac.models import Permission # 白名單列表 WHITE_URL_LIST = [ r'^/login/$', r'^/logout/$', r'^/reg/$', r'^/favicon.ico$', r'^/admin/.*', ] class PermissionMiddleware(MiddlewareMixin): """許可權驗證中介軟體""" def process_request(self, request): # 1.當前訪問的url current_path = request.path_info # 2.白名單判斷,如果在白名單的就直接放過去 for path in WHITE_URL_LIST: if re.search(path, current_path): return None # 3.檢驗當前使用者是否登入 user_id = request.session.get('user_id') if not user_id: return redirect(reverse('login')) # 麵包屑導航欄層級記錄,預設首頁為第一位,主要儲存title(展示在頁面用)和url(使用者點選後可直接跳轉到相應頁面) request.breadcrumb_list = [ { 'title': '首頁', 'url': '/index/', } ] # 4.獲取使用者許可權資訊並進行校驗 permission_list = request.session.get('permission_list') for item in permission_list: # 由於url的是以正則形式儲存,因此採用正則與當前訪問的url進行完全匹配,如果符合則證明有許可權 if re.search('^{}$'.format(item['url']), current_path): # 將當前訪問路徑的所屬選單pk記錄到show_id中,使用者訪問子許可權時依舊會顯示父許可權(二級選單) request.show_id = item['parent_id'] or item['id'] # 將當前訪問的父子資訊記錄到breadcrumb_list中(麵包屑導航欄) # 如果是子許可權的話,就根據父許可權id查出父許可權資訊,將父許可權和子許可權都記錄下來 parent_obj = Permission.objects.filter(pk=item['parent_id']).first() if item['parent_id']: request.breadcrumb_list.extend([ { 'title': parent_obj.title, 'url': parent_obj.url, }, { 'title': item['title'], 'url': item['url'], }]) else: # 排除首頁,因為首頁初始化就存在了 if item['title'] != '首頁': request.breadcrumb_list.append({ 'title': item['title'], 'url': item['url'], }) return None else: return HttpResponse("無此許可權")
4.自定義通用模板(inclusion_tag)
通過上面的校驗後,如果該使用者有許可權則進入系統,並且展示左側選單,但在此時想一下,如果是直接展示的話那麼就意味著每一個檢視函式(django業務邏輯處理相關)都需要返回選單的資料給模板層,因此在這裡就用到了inclusion_tag通用模板,注意:需要新建一個包,名稱必須是templatetags,在包下我新建了一個my_tag.py檔案,存放一下內容
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author: Xiaobai Lei from django.template import Library register = Library() # 獲取左側選單資料給menu.html然後進行展示 @register.inclusion_tag('rbac/menu.html') def get_menu_displays(request): # 獲取選單的字典資料 permission_menu_dict = request.session.get('permission_menu_dict') # 迴圈獲取每個選單資訊 for menu in permission_menu_dict.values(): # 預設二級選單都是隱藏狀態 menu['class'] = 'hide' # 迴圈獲取每個二級選單資訊 for reg in menu['menu_children']: # if re.search("^{}$".format(reg['url']), request.path): # 在中介軟體處理時就已經將父子許可權的show_id都變成了父許可權的id,以此來表示無論操作哪一個,左側父許可權選單都是被選中狀態 if request.show_id == reg['id']: reg['class'] = 'active' # 顯示二級選單 menu['class'] = '' return {'permission_menu_dict': permission_menu_dict} @register.filter def url_is_permission(url, request): """判斷當前按鈕url是否在許可權列表""" permission_url_names = request.session.get('permission_url_names') return url in permission_url_names
5.menu.html(迴圈展示選單)
<div class="multi-menu"> {% for item in permission_menu_dict.values %} <div class="item"> <div class="title"><i style="margin-right: 3px" class="fa {{ item.menu_icon }}"></i>{{ item.menu_title }}</div> <div class="body {{ item.class }}"> {% for menu_chidren in item.menu_chidren %} <a class="{{ menu_chidren.class }}" href="{{ menu_chidren.url }}"> <span class="icon-wrap"><i style="margin-right: 3px" class="fa {{ menu_chidren.icon }}"></i></span>{{ menu_chidren.title }}</a> {% endfor %} </div> </div> {% endfor %} </div>
6.最後需要在業務的html中應用自定義的inclusion_tag
{% load my_tag %} {% get_menu_displays request %}
至此,許可權大體開發完成,目前資料還需要自己去admin管理後臺錄入,下一篇我會繼續說一下開發許可權管理的功能,這樣就能直接在系統上進行使用者,角色和許可權的自由分配了,到時會將許可權和CRM專案合為一體分享源