一、功能分析:
一個成熟的web應用,對權限的控制、管理是不可少的;對於一個web應用來說是什么權限?
這要從web應用的使用說起,用戶在瀏覽器輸入一個url,訪問server端,server端返回這個url下對應的資源;
所以 對於用戶來說 1個可以訪問url 就等於1個權限
比如某人開發了一個web應用包含以下5個url,分別對於不同資源;
1、91.91p15.space/Chinese/
2、91.91p15.space/Japanese and Korean/
3、91p15.space/Euramerican/
4、91p15.space/Latin America/
5、91p15.space/African/
--------------------------------------------------------------------------------------------------------
普通用戶:可以訪問 5
白金用戶:可以訪問 4、5、1
黃金用戶:可以訪問1、2、3、4、5
為什么某些網站會為廣大用戶做角色划分呢(比如 普通、會員、黑金、白金)?
因為給用戶歸類后,便於權限的划分、控制、管理;
所以我們把這種基於角色來做得權限控制,稱為RBAC(Role Basic Access Control)
二、權限管理數據庫表結構設計
1、用戶表:用戶表和角色表為多對多關系,1個用戶可以有多個角色,1個角色可以被多個用戶划分;
2、角色表:角色表和權限也是多對多關系,一個角色可以有多個權限,一個權限可以划分給多個角色
3、菜單表:用於在前端引導用戶找到自己的權限,並可以設置多級菜單對用戶權限進行划分;所以權限表和菜單表是1對多關系;
由於需要構建多級菜單,並且擁有嵌套關系,所以菜單表自引用;
啟發:一般設計包含層級結構嵌套,切嵌套的層級無法預測的表結構使用自關聯;(表1外鍵-----》表2----》外鍵表3是行不通的,因為無法預測嵌套層級的深度)
例如:多級評論(無法預測,評論樹的深度)
三、modal.py數據模型
1、創建一個獨立的app作為公共模塊,以備后期遇到權限相關項目時使用;

from django.db import models from django.db import models class Menu(models.Model): ''' 菜單表''' caption=models.CharField(max_length=32) parent=models.ForeignKey('Menu',null=True,blank=True) #自關聯 def __str__(self): caption_list = [self.caption,] p=self.parent while p: #如果有父級菜單,一直向上尋找 caption_list.insert(0,p.caption) p=p.parent return "-".join(caption_list) class Permission(models.Model): '''權限表''' title = models.CharField(max_length=64) url = models.CharField(max_length=255) menu = models.ForeignKey('Menu', null=True, blank=True)#和菜單是1對多關系 def __str__(self): return '權限名稱: %s--------權限所在菜單 %s'% (self.title,self.menu) class Role(models.Model): '''角色表''' rolename=models.CharField(max_length=32) permission=models.ManyToManyField('Permission') def __str__(self): return '角色: %s--------權限 %s'% (self.rolename,self.permission) class UserInfo(models.Model): '''用戶表''' name=models.CharField(max_length=32) pwd=models.CharField(max_length=64) rule=models.ManyToManyField('Role') def __str__(self): return self.name
四、權限初始化設置、中間件獲取、判斷、生成權限菜單;
當用戶登錄之后獲取到用戶名、密碼查詢用戶表連表查詢得到角色、權限信息,寫入當前用戶session(用session來保存用戶的權限信息);
寫入session之后每次用戶請求到來,通過Django中間件判斷用戶權限;
1.用戶首次登錄,初始時該用戶權限,寫入session;

from app02 import models from app02.service import init_session from django.conf import settings import re def login(reqeust): if reqeust.method == 'GET': return render(reqeust, 'login.html') else: user = reqeust.POST.get('user') pwd = reqeust.POST.get('pwd') user_obj = models.UserInfo.objects.filter(name=user, pwd=pwd).first() if user: # init_session(reqeust,user_obj) init_session.per(reqeust,user_obj)#用戶首次登錄初始化用戶權限信息 return redirect('/index/') else: return render(reqeust, 'login.html') def index(request): return HttpResponse('INDEX') def test_query(request): return render(request,'test.html')

from django.conf import settings from .. import models def per(reqeust,user_obj): permission_list = user_obj.rule.values('permission__title', 'permission__url', 'permission__menu_id', ).distinct() permission_urllist = [] # 當前用戶可以訪問的url(權限列表) permission_menulist = [] # 當前用戶應該掛靠到菜單上顯示的權限 for iteam in permission_list: permission_urllist.append(iteam['permission__url']) if iteam['permission__menu_id']: temp = {'title': iteam['permission__title'], 'url': iteam['permission__url'], 'menu_id': iteam['permission__menu_id']} permission_menulist.append(temp) menulist = list(models.Menu.objects.values('id', 'caption', 'parent_id')) # 獲取所有菜單(以便當前用戶的菜單掛靠) from django.conf import settings reqeust.session[settings.SESSION_PERMISSION_URL_KEY] = permission_urllist reqeust.session[settings.SESSION_PERMISSION_MENU_URL_KEY] = { 'k1': permission_menulist, 'k2': menulist }
2.用戶再次登錄通過Django中間件 檢查當前用戶session中攜帶的權限信息,進而判斷用戶是否對當前request.path有訪問權限?;

from django.utils.deprecation import MiddlewareMixin import re from django.shortcuts import render,redirect,HttpResponse from django.conf import settings class Mddile1(MiddlewareMixin): def process_request(self,request): #如果用戶訪問的url是登錄、注冊頁面,記錄到白名單,放行 for url in settings.PASS_URL_LIST: if re.match(url,request.path_info): return None Permission_url_list=request.session.get(settings.SESSION_PERMISSION_URL_KEY) #如果用戶訪問的url 不在當前用戶權限之內 返回login頁面 if not Permission_url_list: return redirect(settings.LOGIN_URL) current_url=request.path_info #由於數據庫的數據,可能是正則所有 一定要精確匹配 flag=False for url in Permission_url_list: url='^%s$'%(url) if re.match(url,current_url): flag=True break if not flag: if settings.DEBUG: #如果是程序調試應該 顯示用戶可以訪問的權限 url_html='<br/>'.join(Permission_url_list) return HttpResponse('無權訪問您可以訪問%s'%url_html) else: return HttpResponse('沒有權限') def process_response(self, request,response): return response
五、根據用戶權限生成菜單
當用戶使用當前訪問的通過中間件之后,要做的事情只有2步;
1、根據用戶session中的權限列表,生成該用戶的菜單;
2、根據用戶訪問的當前url,把這個菜單 從當前url(權限)從下到上展開;

def test_query(request): menu_permission_list=request.session[settings.SESSION_PERMISSION_MENU_URL_KEY] permission_list=menu_permission_list['k1'] #獲取需要掛靠在菜單上顯示的權限 menu_list=menu_permission_list['k2'] #獲取全部菜單 all_menu_dict={} # status 是用戶全部權限,掛靠顯示的菜單; # open 當前url(權限)對應的父級菜單展開? for item in menu_list: item['child']=[] item['status']=False item['open']=False all_menu_dict[item['id']]=item current_url=request.path_info for row in permission_list: row['status'] = True row['open']=False if re.match('^%s$'% (row['url']),current_url): row['open']=True all_menu_dict[row['menu_id']]['child'].append(row) pid=row['menu_id'] while pid: all_menu_dict[pid]['status']=True pid=all_menu_dict[pid]['parent_id'] if row['open']: PID=row['menu_id'] while PID: all_menu_dict[PID]['open']=True PID=all_menu_dict[PID]['parent_id'] return HttpResponse('OK')
六、自定義模板語言 simple_tag 把用戶菜單渲染到前端

from django.template import Library from django.conf import settings import re,os from django.utils.safestring import mark_safe register=Library() #生成菜單所有數據 def men_data(request): menu_permission_list = request.session[settings.SESSION_PERMISSION_MENU_URL_KEY] permission_list = menu_permission_list['k1'] # 獲取需要掛靠在菜單上顯示的權限 menu_list = menu_permission_list['k2'] # 獲取全部菜單 all_menu_dict = {} # status 是用戶全部權限,掛靠顯示的菜單; # open 當前url(權限)對應的父級菜單展開? # 把用戶所有的權限掛靠到對應的菜單 for item in menu_list: item['child'] = [] item['status'] = False item['open'] = False all_menu_dict[item['id']] = item current_url = request.path_info for row in permission_list: row['status'] = True row['open'] = False if re.match('^%s$' % (row['url']), current_url): row['open'] = True all_menu_dict[row['menu_id']]['child'].append(row) pid = row['menu_id'] while pid: all_menu_dict[pid]['status'] = True pid = all_menu_dict[pid]['parent_id'] if row['open']: PID = row['menu_id'] while PID: all_menu_dict[PID]['open'] = True PID = all_menu_dict[PID]['parent_id'] # 把用戶所有菜單掛父級菜單 res = [] for k, v in all_menu_dict.items(): if not v.get('parent_id'): res.append(v) else: pid = v.get('parent_id') all_menu_dict[pid]['child'].append(v) return res #生成菜單所用HTML def process_menu_html(menu_list): #盛放菜單所用HTML標簽 tpl1 = """ <div class='rbac-menu-item'> <div class='rbac-menu-header'>{0}</div> <div class='rbac-menu-body {2}'>{1}</div> </div> """ #盛放權限的HTML tpl2 = """ <a href='{0}' class='{1}'>{2}</a> """ html='' for item in menu_list: if not item['status']: continue else: if item.get('url') : # 權限 html+= tpl2.format(item['url'],'rbac_active' if item['open'] else '',item['title']) else: #菜單 html+= tpl1.format(item['caption'],process_menu_html(item['child']),''if item['open'] else 'rbac-hide') return mark_safe( html) @register.simple_tag def rbac_menus(request): res= men_data(request) html=process_menu_html(res) return html @register.simple_tag def rbac_css(): file_path = os.path.join('app02', 'theme', 'rbac.css') if os.path.exists(file_path): return mark_safe(open(file_path, 'r', encoding='utf-8').read()) else: raise Exception('rbac主題CSS文件不存在') @register.simple_tag def rbac_js(): file_path = os.path.join('app02', 'theme', 'rbac.js') if os.path.exists(file_path): return mark_safe(open(file_path, 'r', encoding='utf-8').read()) else: raise Exception('rbac主題JavaScript文件不存在')
七、使用 ModelForm組件 填充插件中數據
1、 Modal Form插件的簡單使用
Modal Form 顧名思義 就是把Modal和Form驗證的功能緊密集合起來,實現對數據庫數據的增加、編輯操作;
添加

from app02 import models from django.forms import ModelForm class UserModalForm(ModelForm): class Meta: model=models.UserInfo #(該字段必須為 model 數據庫中表) fields= '__all__' #(該字段必須為 fields 數據庫中表) def add(request): # 實例化models_form if request.method=='GET': obj = UserModalForm() return render(request,'rbac/user_add.html',locals()) else: obj=UserModalForm(request.POST) if obj.is_valid(): data=obj.cleaned_data obj.save() #form驗證通過直接 添加用戶信息到數據庫 return render(request, 'rbac/user_add.html', locals())
使用

def user_edit(request): pk = request.GET.get('id') user_obj = models.UserInfo.objects.filter(id=pk).first() if request.method=='GET': if not user_obj: return redirect('/app02/user_edit/') else: #在form表單中自動填充默認值 model_form_obj=UserModalForm(instance=user_obj) return render(request,'rbac/user_edit.html',locals()) else: #修改數據 需要instance=user_obj model_form_obj = UserModalForm(request.POST,instance=user_obj) if model_form_obj.is_valid(): model_form_obj.save() return redirect('/app02/userinfo/')
2、Modal Form 參數設置

from django.shortcuts import render,HttpResponse,redirect from app02 import models from django.forms import ModelForm from django.forms import widgets as wid from django.forms import fields as fid class UserModalForm(ModelForm): class Meta: model=models.UserInfo #(該字段必須為 model 數據庫中表) fields= '__all__' #(該字段必須為 fields '__all__',顯示數據庫中所有字段, # fields=['指定字段'] # exclude=['排除指定字段'] ) # fields=['name',] # exclude=['pwd'] #error_messages 自定制錯誤信息 error_messages={'name':{'required':'用戶名不能為空'}, 'pwd': {'required': '密碼不能為空'}, } #widgets 自定制插件 # widgets={'name':wid.Textarea(attrs={'class':'c2'})} #由於數據庫里的字段 和前端顯示的會有差異,可以使用 labels 定制前端顯示 labels={'name':'姓名','pwd':'密碼','rule':'角色'} #自定制 input標簽 輸入信息提示 help_texts={'name':'別瞎寫,瞎寫打你哦!'} #自定制自己 form 字段.CharField() email()等 field_classes={ 'name':fid.CharField }
3、添加數據庫之外的字段,實時數據更新
ModelForm 可以結合Model把所有數據庫字段在頁面上生成,也可以增加額外的字段;
規則:如果增加的字段和數據里的filed重名則覆蓋,不重名則新增;
也可以通過重寫__init__ ,每次實例化1個form對象,實時更新數據;

class PermissionModelForm(ModelForm): #ModelForm 可以結合Model把所有數據庫字段在頁面上生成,也可以增加額外的字段; url=fields.ChoiceField() class Meta: fields = "__all__" model = models.Permission #注意不是models def __init__(self,*args,**kwargs): #重寫父類的 __init__方法,每次實例化實時更新 form中的數據 super(PermissionModelForm,self).__init__(*args,**kwargs) from pro_crm.urls import urlpatterns self.fields['url'].choices=get_all_url(urlpatterns,'/', True)
八、總結
如何把權限精確到按鈕,按鈕就是子菜單就是一個url
權限管理的思路是
把用戶權限記錄到數據庫里面
當用戶首次登錄時,從數據庫里取出數據把用戶的權限(url)和掛靠的菜單菜單/寫入到session中
以后每次訪問在中間件進行check;
難度在於:多級菜單之間的拼接掛靠會用到遞歸,所以我選擇了二級菜單;