1、需求分析:
用戶登陸
- 獲取當前用戶具備的全部角色
- 獲取當前用戶具備的全部權限(去重)html
-在訪問列表頁面時,須要判斷:有無添加權限,有無刪除權限,有無編輯權限;在頁面上顯示相應權限按鈕前端
-訪問菜單URL時,默認展開其所屬菜單python
-訪問非菜單URL,默認選中原菜單jquery
2、方法步驟
a.首先建一個名爲rbac的APP
b.建立表結構,基於角色權限控制、菜單:
-5個類,7張表django

from django.db import models class Menu(models.Model): """ 菜單組 """ title = models.CharField(max_length=32) class Meta: verbose_name_plural = "菜單組" def __str__(self): return self.title class Group(models.Model): """ 權限組 """ caption = models.CharField(verbose_name='組名稱', max_length=16) menu = models.ForeignKey(verbose_name='所屬菜單', to='Menu', default=1) class Meta: verbose_name_plural = "權限組" def __str__(self): return self.caption class Permission(models.Model): """ 權限表 """ title = models.CharField(verbose_name='標題', max_length=32) url = models.CharField(verbose_name="含正則URL", max_length=64) menu_gp = models.ForeignKey(verbose_name='組內菜單', to='Permission', null=True, blank=True, related_name='x1') code = models.CharField(verbose_name="操做", max_length=16) group = models.ForeignKey(verbose_name='所屬組', to="Group") class Meta: verbose_name_plural = "權限表" def __str__(self): return self.title class User(models.Model): """ 用戶表 """ username = models.CharField(verbose_name='用戶名', max_length=32) password = models.CharField(verbose_name='密碼', max_length=64) email = models.CharField(verbose_name='郵箱', max_length=32) roles = models.ManyToManyField(verbose_name='具備的全部角色', to="Role", blank=True) class Meta: verbose_name_plural = "用戶表" def __str__(self): return self.username class Role(models.Model): """ 角色表 """ title = models.CharField(max_length=32) permissions = models.ManyToManyField(verbose_name='具備的全部權限', to='Permission', blank=True) class Meta: verbose_name_plural = "角色表" def __str__(self): return self.title
verbose_name---在admin模式下,字段顯示的名字
blank=True ---在admin模式下,字段能夠爲空
class Meta:
verbose_name_plural = "表名" ---在admin模式下,顯示的表名
爲了讓咱們的表能在admin模式下顯示,咱們須要在rbac文件下的admin.py中,進行以下設置session

from django.contrib import admin from . import models admin.site.register(models.Permission) admin.site.register(models.User) admin.site.register(models.Role) admin.site.register(models.Group) admin.site.register(models.Menu)
c.基於Django admin錄入權限數據
python manage.py createsuperuser數據結構
d.用戶登陸程序
按照邏輯,當咱們輸入一個URL,在跳轉到相應頁面前,咱們須要作3件事:app
1.首先應該先判斷此URL,是否限制用戶瀏覽,若是沒有限制,則不須要登陸,直接進入相應頁面(可在settings中設置白名單實現)ide
2.若限制了,則須要檢驗用戶的訪問權限,在檢驗用戶權限以前,咱們得先檢驗用戶登陸狀態(經過判斷登錄成功後設置的session值是否存在),拿到用戶權限信息(先比對帳戶密碼,正確則設置一個session,用做檢驗登錄狀態)
3.若是是登陸的狀態,咱們還須要檢驗用戶是否有瀏覽此頁面的權限(爲此咱們須要把當前URL和用戶全部權限裏的URL進行比對),如能訪問此URL,當進入URL後,咱們還須要根據用戶的身份,在頁面上顯示不一樣的操做權限(如增刪改查,有哪些操做權限就讓哪些操做權限顯示,沒有的就不讓其顯示出來)
因爲每次跳轉一個URL前,都會重複上面的步驟,咱們能夠將上面的方法代碼寫入中間件中,以避免爲此給每一個視圖函數都加上一個裝飾器(麻煩)
比對帳戶密碼,設置session的過程應該是在視圖函數裏完成,因爲咱們在中間件中會檢驗登錄狀態時會用到session,因此咱們能夠用戶的一些權限信息放到session中
如今咱們須要考慮的是session中到底須要存放哪些信息?
首先,咱們來考慮一下,做爲權限管理系統,咱們但願能根據用戶的權限,在頁面顯示一些相應菜單信息和操做按鈕,因此session設置,咱們分兩個部分:
一個是權限相關,用來檢驗用戶訪問權限,以及頁面權限相關按鈕(添加、編輯、刪除)顯示與否
二是菜單相關,好比咱們點擊菜單一里的子選項:用戶列表,顯示頁面信息後,菜單一仍然是展開狀態,當咱們在用戶列表點擊添加用戶,跳轉新頁面時,仍然展開菜單一
session權限相關
根據需求,咱們知道,權限相關裏,咱們須要拿到用戶可訪問的全部頁面(url,用來匹配訪問權限)、用戶全部可執行的操做(code,用來匹配可顯示的操做鍵)、和(permissions__group_id)用來把權限歸類,
爲了方便後取數據,因此咱們最終想要的結構是
{group_id1:{
urls:[url一、url二、url3....],
codes:[list、del、edit、add]},
group_id2:{
urls:[url一、url二、url3....],
codes:[list、del、edit、add]}
session菜單相關
根據需求,咱們知道,權限相關裏,咱們須要menu_title(大菜單標題),menu_id(大菜單的id,用來經過此id歸類子菜單),menu_gp_id(組內id,用於判斷是否能夠做爲子菜單,以及非子菜單所屬的子菜單),permission_id(經過可作菜單的id,分組其下面的url),permission_title(子菜單的標題)、url(用於放在a標籤裏做跳轉連接,若是不是子菜單,則打開的是此連接,其所屬組所在菜單欄應該展開)
因此咱們一共要取出
list = user.roles.values('permissions__code', # 權限操做 'permissions__title', # 權限名 'permissions__url', # 權限url 'permissions__id', # 權限id 'permissions__menu_gp_id', # 組內菜單id 'permissions__group_id', # 權限組id 'permissions__group__menu__id', # 菜單id 'permissions__group__menu__caption', # 菜單名 ).distinct()
必定要去重,由於有的人有幾個身份,有的權限會重複,將上面拿到的數據,再處理一下,分別做爲,菜單相關,權限相關所需的數據
因爲以上步驟過多,咱們應該從視圖函數裏抽離出來,單獨在rbac文件夾下建一個service文件夾,裏面建一個名爲init_permission.py的初始化文件

from django.conf import settings def init_permission(user,request): """ 初始化權限信息,獲取權限信息並放置到session中。 """ permission_list = user.roles.values('permissions__id', 'permissions__title', # 用戶列表 'permissions__url', 'permissions__code', 'permissions__menu_gp_id', # 組內菜單ID,Null表示是菜單 'permissions__group_id', 'permissions__group__menu_id', # 菜單ID 'permissions__group__menu__title',# 菜單名稱 ).distinct() #要去重,由於每一個人可能有多種身份,權限會重複 # 菜單相關(之後再匹配) sub_permission_list = [] for item in permission_list: tpl = { 'id':item['permissions__id'], 'title':item['permissions__title'], 'url':item['permissions__url'], 'menu_gp_id':item['permissions__menu_gp_id'], 'menu_id':item['permissions__group__menu_id'], 'menu_title':item['permissions__group__menu__title'], } sub_permission_list.append(tpl) request.session[settings.PERMISSION_MENU_KEY] = sub_permission_list # 權限相關 result = {} for item in permission_list: group_id = item['permissions__group_id'] code = item['permissions__code'] url = item['permissions__url'] if group_id in result: result[group_id]['codes'].append(code) result[group_id]['urls'].append(url) else: result[group_id] = { 'codes':[code,], 'urls':[url,] } request.session[settings.PERMISSION_URL_DICT_KEY] = result
咱們只須要在login的視圖函數裏驗證完用戶名密碼後,調用函數就行
init_permission(user,request)
要記得先導入,才能成功調用
from rbac.service.init_permission import init_permission
這樣咱們就完成了session值的設置,接下來咱們能夠寫咱們的中間件了
在rbac中創建一個middlewares文件夾,而後建一個名爲rbac.py的文件,用來編寫咱們自定義的中間件代碼

import re from django.shortcuts import redirect,HttpResponse from django.conf import settings class MiddlewareMixin(object): def __init__(self, get_response=None): self.get_response = get_response super(MiddlewareMixin, self).__init__() def __call__(self, request): response = None if hasattr(self, 'process_request'): response = self.process_request(request) if not response: response = self.get_response(request) if hasattr(self, 'process_response'): response = self.process_response(request, response) return response class RbacMiddleware(MiddlewareMixin): def process_request(self,request): # 1. 獲取當前請求的URL # request.path_info # 2. 獲取Session中保存當前用戶的權限 # request.session.get("permission_url_list') current_url = request.path_info # 當前請求不須要執行權限驗證 for url in settings.VALID_URL: if re.match(url,current_url): return None #在視圖函數那邊驗證用戶登陸成功後,會設置session,若是登陸成功則會有此session permission_dict = request.session.get(settings.PERMISSION_URL_DICT_KEY) if not permission_dict: #沒有session值,則說明沒有登陸,須要跳轉登陸頁面 return redirect('/login/') flag = False for group_id,code_url in permission_dict.items(): for db_url in code_url['urls']: regax = "^{0}$".format(db_url) #爲了精準匹配 # 將當前頁面與當前用戶可訪問的頁面進行匹配 if re.match(regax, current_url): #匹配成功後將用戶的可執行的(增、刪、改、列表等操做)存到request中,方便調用 request.permission_code_list = code_url['codes'] flag = True break if flag: break #匹配失敗,則顯示無權訪問 if not flag: return HttpResponse('無權訪問')
寫完自定義中間件後,記得去settings.py中加上咱們寫的中間件,來讓咱們的中間件生效
MIDDLEWARE = ['rbac.middlewares.rbac.RbacMiddleware',]
在settings中,咱們還須要寫上咱們自定義的字符串
PERMISSION_URL_DICT_KEY = "permission_url_dict"
之因此這樣寫,是爲了方便之後修改時,只須要修改settings裏的值,其餘引用到的地方都會自動應用
還有加上URL白名單
VALID_URL = [ "/login/", "/admin.*" ]
因爲頁面上都會顯示菜單,因此咱們先寫一個模板,其餘的只有繼承就好

{% load rbac %} {# 引用自定義標籤 #} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/static/rbac/rbac.css" /> <title>Title</title> </head> <body> <div style="float: left;width: 20%;height: 500px;background-color: #dddddd"> {% menu_html request %} {# 調用自定義menu_html標籤,並傳入request參數 #} </div> <div style="float: left;width: 80%"> {% block content %}{% endblock %} </div> <script src="/static/jquery-3.2.1.min.js"></script> <script src="/static/rbac/rbac.js"></script> </body> </html>
在rbac下創建一個static再在裏面創建rbac文件夾,在建咱們的css和js文件

.item-permission{ padding: 3px 10px; } .item-permission a{ display: block; } .item-permission a.active{ color: red; } .hide{ display: none; }

/** * Created by Administrator on 2017/11/8. */ $(function () { $('.item-title').click(function () { if($(this).next().hasClass('hide')){ $(this).next().removeClass('hide') }else{ $(this).next().addClass('hide') } }) });
注意根目錄下必定要有static文件下,不然咱們的css,JS可能不生效
jquery文件咱們能夠放在根目錄下的static文件夾中
接下來咱們寫自定義標籤的內容在rbac下創建templatetags文件下,建立rbac.py

import re from django.template import Library from django.conf import settings register = Library() @register.inclusion_tag("xxxxx.html") def menu_html(request): """ 去Session中獲取菜單相關信息,匹配當前URL,生成菜單 :param request: :return: """ menu_list = request.session[settings.PERMISSION_MENU_KEY] current_url = request.path_info menu_dict = {} for item in menu_list: if not item['menu_gp_id']: menu_dict[item['id']] = item 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 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} ] } return {'menu_dict':result}
此自定義標籤中函數的做用是去Session中獲取菜單相關信息,匹配當前URL,生成菜單
1.首先咱們須要從用戶的權限中拿出菜單相關的信息,並獲取當前訪問的URL
menu_list = request.session[settings.PERMISSION_MENU_KEY]
current_url = request.path_info
2.從menu_list中獲取用戶權限裏能做爲菜單的權限:
menu_dict = {} for item in menu_list: if not item['menu_gp_id']: menu_dict[item['id']] = item
3.匹配當前URL,判斷當前URL是否可作菜單,若是能夠,則展開此菜單,若是不是菜單,則將其所屬菜單展開(給菜單加狀態)
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
4,菜單相關信息已經整理完畢,存在了menu_dict中,接下來就要建立菜單結構,將菜單數據結構化,將同屬一個大菜單下的菜單鍵放一個大菜單下
result = {} for item in menu_dict.values(): active = item.get('active') menu_id = item['menu_id'] if menu_id in result: #一個大菜單下有多個子菜單,則只須要將子菜單的item和url以及狀態加入便可 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} ] }
@register.inclusion_tag("xxxxx.html")做用是:能夠將其所裝飾的函數menu_html的返回值直接給xxxxx.html(此頁面是用來生成菜單相關的html)中使用,
當調用自定義標籤menu_html時,會將通過渲染後的xxxxx.html頁面以字符串的形式返回給調用的地方
用來造成菜單
這樣之後其餘頁面直接繼承layout.html就會有其對應的菜單,內容部分,咱們還須要判斷一下用戶的操做權限,以顯示相應操做按鈕,如userinfo頁面

from django.conf.urls import url from django.contrib import admin from rbac import views from app01 import views as app01_views urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^test/', views.test), url(r'^login/', app01_views.login), url(r'^index/', app01_views.index), url(r'^userinfo/$', app01_views.userinfo), url(r'^userinfo/add/$', app01_views.userinfo_add), url(r'^order/$', app01_views.order), url(r'^order/add/$', app01_views.order_add), ]

class BasePagePermission(object): #定義這個類,是爲了html頁面方便調用判斷 def __init__(self,code_list): self.code_list = code_list def has_add(self): if "add" in self.code_list: return True def has_edit(self): if 'edit' in self.code_list: return True def has_del(self): if 'del' in self.code_list: return True def userinfo(request): page_permission = BasePagePermission(request.permission_code_list) data_list = [ {'id':1,'name':'xxx1'}, {'id':2,'name':'xxx2'}, {'id':3,'name':'xxx3'}, {'id':4,'name':'xxx4'}, {'id':5,'name':'xxx5'}, ] return render(request,'userinfo.html',{'data_list':data_list,'page_permission':page_permission}) def userinfo_add(request): page_permission = BasePagePermission(request.permission_code_list) return render(request, 'userinfo_add.html', { 'page_permission': page_permission}) class OrderPagePermission(BasePagePermission): #繼承基礎的,方便擴展 def has_report(self): if 'report' in self.code_list: return True def order(request): order_permission = OrderPagePermission(request.permission_code_list) return render(request,'order.html')

{% extends "layout.html" %} {% 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 %}
3、總結
rbac裏有什麼?
settings中有什麼?
#########註冊rbac的app,使其生效######## INSTALLED_APPS = [ 'rbac.apps.RbacConfig', ###也可直接寫rbac ] #########加入咱們自定義的中間件,使其生效######## MIDDLEWARE = [ 'rbac.middlewares.rbac.RbacMiddleware', ] #############CSS,JS,JQ############ STATICFILES_DIRS = ( os.path.join(BASE_DIR,'static'), ) ##########Session中用到的變量############# PERMISSION_URL_DICT_KEY = "permission_url_dict" PERMISSION_MENU_KEY = "permission_menu_key" #####白名單############ VALID_URL = [ "/login/", "/admin.*" ]
使用方法
1.建立project
2.複製rbac文件
3.在settings中按上面的設置
4.去admin中配置文件,錄入權限信息
5.在登錄邏輯中,登錄成功時,調用init_permission方法(爲此要在視圖函數中導入
from rbac import models
from rbac.service.init_permission import init_permission
)
6.視圖函數中用來判斷是否具備code操做
class BasePagePermission(object): #定義這個類,是爲了html頁面方便調用判斷 def __init__(self,code_list): self.code_list = code_list def has_add(self): if "add" in self.code_list: return True def has_edit(self): if 'edit' in self.code_list: return True def has_del(self): if 'del' in self.code_list: return True def userinfo(request): page_permission = BasePagePermission(request.permission_code_list) data_list = [ {'id':1,'name':'xxx1'}, {'id':2,'name':'xxx2'}, {'id':3,'name':'xxx3'}, {'id':4,'name':'xxx4'}, {'id':5,'name':'xxx5'}, ] return render(request,'userinfo.html',{'data_list':data_list,'page_permission':page_permission}) def userinfo_add(request): page_permission = BasePagePermission(request.permission_code_list) return render(request, 'userinfo_add.html', { 'page_permission': page_permission}) class OrderPagePermission(BasePagePermission): #繼承基礎的,方便擴展 def has_report(self): if 'report' in self.code_list: return True def order(request): order_permission = OrderPagePermission(request.permission_code_list) return render(request,'order.html')
前端頁面只須要
{% if page_permission.has_add %}
<a href="/userinfo/add/">添加</a>
{% endif %}
7. 模板中:
{%load rbac %} 加載自定義標籤
<link rel="stylesheet" href="/static/rbac/rbac.css" /> 引用rbac中的CSS
{% menu_html request %} 調用自定義menu_html標籤,並傳入request參數
<script src="/static/jquery-3.2.1.min.js"></script><script src="/static/rbac/rbac.js"></script> 引用rbac中的JS