權限組件
在咱們寫項目時,可能會遇到給不一樣的用戶分配不一樣的權限的狀況,那麼什麼是權限呢?權限其實就是一個urlcss
不一樣的url表明不一樣的功能,限定用戶能訪問的url,就給了用戶不一樣的權限html
權限管理在不少項目中都有用到,因此咱們能夠講權限管理的邏輯寫成一個組件前端
使它在不一樣的項目中只要通過必定的修改就能使用正則表達式
建立項目
在一個項目中能夠包含多個組件,一樣一個組件也能夠用於多個項目,這裏咱們想建立一個項目數據庫
而後建立兩個app,一個app01寫項目的主邏輯,一個rbac(Role-Based Access Control)寫權限相關的邏輯django
建表
咱們首先能想到的是用戶和權限表,一個用戶能夠有多個權限,而一個權限也能夠對應多個用戶,這樣他們就是多對多的關係bootstrap
可是這樣的話,咱們會發現一個問題,好比一個公司有不少銷售員,這些銷售員都有一樣的權限,這時咱們就須要添加不少重複的信息session
這裏咱們能夠再添加一張角色表,讓角色和用戶、權限都有多對多的關係app
這樣有新的用戶後,只要給該用戶分配一個角色就好了,而該角色又擁有相應的權限,就不須要再添加劇復的信息了ide
rbac.models
from django.db import models # Create your models here. class UserInfo(models.Model): name = models.CharField(max_length=32) pwd = models.CharField(max_length=32, default=123) email = models.EmailField() roles = models.ManyToManyField(to="Role") def __str__(self): return self.name class Role(models.Model): title = models.CharField(max_length=32) permissions = models.ManyToManyField(to="Permission") def __str__(self): return self.title class Permission(models.Model): url = models.CharField(max_length=32) title = models.CharField(max_length=32) def __str__(self): return self.title
表建好後咱們就要往表中添加數據了,這裏咱們能夠先用admin添加一些基本數據
rbac.admin
from django.contrib import admin from .models import * # Register your models here. admin.site.register(UserInfo) admin.site.register(Role) class PermissionConfig(admin.ModelAdmin): list_display = ["id", "title", "url"] ordering = ["id"] admin.site.register(Permission, PermissionConfig)
通常狀況下,咱們添加一條數據會看到一個數據對象,若是想要展現具體的id,title等屬性,咱們可使用上面的方法,建立一個新的類繼承(admin.ModelAdmin),在裏面定義list_display,講想要看到的內容寫入列表,而後使用admin.site.register(Permission, PermissionConfig),這樣咱們就能夠在頁面上看到以下效果
這裏咱們添加了8個權限,咱們能夠發現這8個權限能夠分爲兩組,前4個是和用戶有關的,然後4個是和訂單有關的
這裏咱們能夠再建立一張權限組的表,使他和權限表一對多關聯,同時給權限表再建立一個編號
rbac.models
from django.db import models # Create your models here. class UserInfo(models.Model): name = models.CharField(max_length=32) pwd = models.CharField(max_length=32, default=123) email = models.EmailField() roles = models.ManyToManyField(to="Role") def __str__(self): return self.name class Role(models.Model): title = models.CharField(max_length=32) permissions = models.ManyToManyField(to="Permission") def __str__(self): return self.title class Permission(models.Model): url = models.CharField(max_length=32) title = models.CharField(max_length=32) permission_group = models.ForeignKey("PermissionGroup", default=1) code = models.CharField(max_length=32, default="") def __str__(self): return self.title class PermissionGroup(models.Model): caption = models.CharField(max_length=32) def __str__(self): return self.caption
rbac.admin
from django.contrib import admin from .models import * # Register your models here. admin.site.register(UserInfo) admin.site.register(Role) admin.site.register(PermissionGroup) class PermissionConfig(admin.ModelAdmin): list_display = ["id", "title", "url", "permission_group", "code"] ordering = ["id"] admin.site.register(Permission, PermissionConfig)
最後添加的權限以下
用戶登陸
urls
from django.conf.urls import url from django.contrib import admin from app01 import views urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^login/', views.login), url(r'^users/', views.users), url(r'^orders/', views.orders), url(r'^orders/add/', views.orders_add), ]
這裏咱們爲一些相關權限也設置了url
視圖函數
因爲這些是關於項目的邏輯,因此視圖函數寫在app01.views
from django.shortcuts import render, redirect, HttpResponse from rbac import models # Create your views here. def login(request): if request.method == "GET": return render(request, "login.html") else: user = request.POST.get("user") pwd = request.POST.get("pwd") user = models.UserInfo.objects.filter(name=user, pwd=pwd).first() if user: # 驗證成功以後 request.session["user_id"] = user.pk # 當前登陸用戶的全部權限 from rbac.service.initial import initial_session initial_session(request, user) return HttpResponse("登陸成功") else: return redirect("/login/") def users(request): return HttpResponse("用戶列表") def orders(request): permission_dict = request.session.get("permission_dict") return render(request, "orders.html", locals()) def orders_add(request): return HttpResponse("添加訂單")
login.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>登陸</title> </head> <body> <form action="/login/" method="post"> {% csrf_token %} <p>用戶名 <input type="text" name="user"></p> <p>密碼 <input type="password" name="pwd"></p> <input type="submit" value="提交"> </form> </body> </html>
能夠看到當用戶登陸之後咱們先驗證帳號密碼是否正確,而後寫session,這裏咱們調用了rbac內的方法
from rbac.service.initial import initial_session initial_session(request, user)
這個方法究竟作了什麼呢
def initial_session(request, user): # 方式1 # permission_info = user.roles.all().values("permissions__url", "permissions__title").distinct() # temp = [] # for i in permission_info: # temp.append(i["permissions__url"]) # request.session["permission_list"] = temp # 方式2 # 建立一個數據格式:包含全部權限,權限所在組,權限的編號 permission_info = user.roles.all().values("permissions__url", "permissions__code", "permissions__permission_group_id").distinct() permission_dict = {} for permission in permission_info: if permission["permissions__permission_group_id"] in permission_dict: permission_dict[permission["permissions__permission_group_id"]]["urls"].append( permission["permissions__url"]) permission_dict[permission["permissions__permission_group_id"]]["codes"].append( permission["permissions__code"]) else: permission_dict[permission["permissions__permission_group_id"]] = { "urls": [permission["permissions__url"]], "codes": [permission["permissions__code"]] } ''' permission_dict = { 1:{}, 2:{ "urls": [], "codes": [] } } ''' request.session["permission_dict"] = permission_dict
咱們能夠看到這個方法有兩種定義session數據的方式,方式1只是簡單的取出數據庫中用戶擁有權限的url,並添加到一個列表中
而方式2定義了一個如註釋中所見的數據形式,將相關數據寫入session中後,當用戶訪問一個url時,咱們就能夠從session中取出該用戶擁有的權限,再驗證一下訪問的url是否在用戶的權限中,若是在,那麼就讓他經過,不在則返回無權訪問
咱們發現,不論用戶訪問哪一個url咱們都應該作權限的驗證,這時就須要使用中間件來解決驗證問題了
中間件
from django.utils.deprecation import MiddlewareMixin from django.shortcuts import render, redirect, HttpResponse import re class M1(MiddlewareMixin): def process_request(self, request): current_path = request.path_info # 白名單,當用戶訪問如下url時直接經過 valid_url_menu = ["/login/", "/reg/", "/admin/.*"] for valid_url in valid_url_menu: ret = re.match(valid_url, current_path) if ret: return None # 方式1 # permissions_list = request.session.get("permission_list") # 方式2 permission_dict = request.session.get("permission_dict") if not request.session.get("user_id"): return redirect("/login/") for item in permission_dict.values(): regs = item["urls"] codes = item["codes"] for reg in regs: reg = "^%s$" % reg ret = re.match(reg, current_path) if ret: request.permission_codes = codes return None return HttpResponse("無權訪問") # 方式1 # flag = False # for permission_url in permissions_list: # permission_url = "^%s$" % permission_url # ret = re.match(permission_url, current_path) # if ret: # flag = True # break # if not flag: # return HttpResponse("無權訪問")
當用戶訪問時,先取到用戶訪問的url:request.path_info
有些url咱們應該讓每一個用戶都能訪問,好比登陸、註冊頁面等
因此咱們設置一個白名單,若是用戶訪問的url在白名單內,則直接經過
若是不在,咱們從request.session中取到用戶相關的權限,根據咱們設置的數據類型,驗證用戶訪問的url是否在權限內
這裏要注意,上面咱們提到權限是一個url,這裏咱們還要補充一下,權限是一個包含正則的url,因此在驗證是,咱們用到了re模塊進行正則匹配,同時爲了配置的精準,咱們在權限先後分別加上了^和$符
當匹配成功後咱們將url對應的code列表存到request中,並在中間件中放行
頁面上數據的使用
若是用上面的方式1存數據,那麼咱們能拿到的只是一個url的列表,其中還包含正則表達式,在頁面上使用時咱們不能使用正則匹配,因此沒法進行判斷
因此咱們使用方式2的數據
視圖函數
def orders(request): permission_dict = request.session.get("permission_dict") permission_codes = request.permission_codes per = Permissions(permission_codes) return render(request, "orders.html", locals())
這裏的Permissions是咱們本身定義的一個類,類中定義了判斷增刪改查是否在permission_codes中的方法,方便咱們在前端使用
class Permissions(object): def __init__(self, code_list): self.code_list = code_list def list(self): return "list" in self.code_list def add(self): return "add" in self.code_list def delete(self): return "delete" in self.code_list def edit(self): return "edit" in self.code_list
前端頁面
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css"> </head> <body> <h3>訂單列表</h3> <div class="col-md-6"> {% if per.add %} <p><a href="/orders/add/"><button class="btn btn-primary pull-right">添加訂單</button></a></p> {% endif %} <table class="table table-stripped"> <tr> <th>訂單號</th> <th>訂單日期</th> <th>商品名稱</th> <th>操做</th> </tr> <tr> <td>12343</td> <td>2012-12-12</td> <td>li</td> {% if per.delete %} <td><a href=""><button class="btn btn-danger btn-sm">刪除</button></a></td> {% endif %} </tr> </table> </div> </body> </html>
前端咱們只須要使用per這個對象的方法就能直接判斷當前用戶有沒有對應的權限,從而進行頁面渲染
權限菜單
上面的方法咱們經過判斷用戶的權限,在頁面上給用戶顯示響應的按鈕
如今咱們又有了新的需求,要在頁面上顯示一個左側菜單,菜單中有不一樣的菜單欄,每個菜單欄中都對應有用戶有的相應權限
當用戶訪問某一個url時,url對應的權限會變色,同時該權限所在的菜單欄會是打開狀態,其它菜單欄是關閉狀態
若是經過咱們以前的表結構設計,這個菜單欄對用的其實就是權限組,這時會有一個問題,當咱們顯示權限菜單時,刪除和編輯權限所對應的url是一個正則表達式,這樣的權限其實咱們是不該該顯示在菜單欄的
在展現時咱們須要經過判斷將這些權限給排除,這個過程其實比較簡單,可是咱們要考慮,當咱們訪問這些權限時,權限菜單中的哪一個權限應該變色呢,我以爲應該是刪除和編輯對應的展現列表權限應該變色
這時咱們會發現咱們每個菜單欄其實就是一個權限組,而裏面對應的權限其實不多,這種狀況下當權限組較多時,咱們的菜單也會不少
這種二級菜單的顯示效果就不是很好了,因此咱們對錶結構進行一些修改
rbac.models
from django.db import models # Create your models here. class Menu(models.Model): caption = models.CharField(max_length=32) def __str__(self): return self.caption class UserInfo(models.Model): name = models.CharField(max_length=32) pwd = models.CharField(max_length=32, default=123) email = models.EmailField() roles = models.ManyToManyField(to="Role") def __str__(self): return self.name class Role(models.Model): title = models.CharField(max_length=32) permissions = models.ManyToManyField(to="Permission") def __str__(self): return self.title class Permission(models.Model): url = models.CharField(max_length=32) title = models.CharField(max_length=32) permission_group = models.ForeignKey("PermissionGroup", default=1) code = models.CharField(max_length=32, default="") parent = models.ForeignKey("self", default=1, null=True, blank=True) def __str__(self): return self.title class PermissionGroup(models.Model): caption = models.CharField(max_length=32) menu = models.ForeignKey("Menu", default=1) def __str__(self): return self.caption
咱們增長了一個菜單表,該表一對多對應權限組表,同時咱們在權限表中增長了一個新的字段parent,這個字段自關聯本身,當某個權限不須要在菜單欄展現,咱們就給他一個parent,這樣訪問他時,對應的parent權限在菜單欄就會變色
修改了表結構之後,咱們的菜單、權限組和權限如今是一個三級菜單的關係
菜單
權限組
權限
咱們展現時只展現菜單和權限,不考慮權限組,這時咱們須要考慮的是須要拿到怎樣的數據在頁面上進行展現
menu_dict = { 1: { "title": "菜單一", "active": False, "children": [ {"title": "添加用戶", "url": "xxxxxxxxxxx", "active": False}, {"title": "查看用戶", "url": "xxxxxxxxxxx", "active": False}, ]}, 2: { "title": "菜單二", "active": True, "children": [ {"title": "添加用戶", "url": "xxxxxxxxxxx", "active": False}, {"title": "查看用戶", "url": "xxxxxxxxxxx", "active": True}, ] }}
上面的數據中,字典的key(一、2)表示的是菜單的id,active表示的是菜單是不是打開狀態,children列表中放的是菜單擁有的權限,權限的active表示的是權限是否變色
有了這樣的數據咱們能夠經過下面的方法在頁面上進行渲染
<div class="menu"> {% for item in menu_dict.values %} <div class="item"> <div class="title"><a href="">{{ item.title }}</a></div> {% if item.active %} <div class="con"> {% else %} <div class="con hide"> {% endif %} {% for son in item.children %} {% if son.active %} <p><a href="{{ son.url }}" class="active">{{ son.title }}</a></p> {% else %} <p><a href="{{ son.url }}">{{ son.title }}</a></p> {% endif %} {% endfor %} </div> </div> {% endfor %} </div> <div class="content"> {% block con %} {% endblock %} </div> </div>
如今的問題就是咱們要得到這樣的數據類型,首先用戶登陸時,咱們能夠先從數據庫中取出咱們須要的數據,放到session中
修改rbac.initial.py
def initial_session(request, user): # 方式1 # permission_info = user.roles.all().values("permissions__url", "permissions__title").distinct() # temp = [] # for i in permission_info: # temp.append(i["permissions__url"]) # request.session["permission_list"] = temp # 方式2 # 建立一個數據格式:包含全部權限,權限所在組,權限的編號 permission_info = user.roles.all().values("permissions__url", "permissions__code", "permissions__permission_group_id").distinct() permission_dict = {} for permission in permission_info: if permission["permissions__permission_group_id"] in permission_dict: permission_dict[permission["permissions__permission_group_id"]]["urls"].append( permission["permissions__url"]) permission_dict[permission["permissions__permission_group_id"]]["codes"].append( permission["permissions__code"]) else: permission_dict[permission["permissions__permission_group_id"]] = { "urls": [permission["permissions__url"]], "codes": [permission["permissions__code"]] } ''' permission_dict = { 1:{}, 2:{ "urls": [], "codes": [] } } ''' request.session["permission_dict"] = permission_dict # 建立生成菜單的數據 permission_info = user.roles.all().values("permissions__url", "permissions__code", "permissions__permission_group_id", "permissions__parent_id", "permissions__permission_group__menu__id", "permissions__permission_group__menu__caption", "permissions__title", "permissions__id").distinct() permission_list = [] for permission_item in permission_info: temp = { "id": permission_item["permissions__id"], "url": permission_item["permissions__url"], "title": permission_item["permissions__title"], "pid": permission_item["permissions__parent_id"], "menu_name": permission_item["permissions__permission_group__menu__caption"], "menu_id": permission_item["permissions__permission_group__menu__id"] } permission_list.append(temp) request.session["permission_list"] = permission_list
上面的permission_list是咱們取到的原始數據,如今咱們要將該數據處理成咱們理想的結構
def get_menu(request): permission_list = request.session.get("permission_list") # 存儲全部放到菜單欄中的權限 temp_dict = {} for item in permission_list: pid = item["pid"] if not pid: item["active"] = False temp_dict[item["id"]] = item # 將須要標中的active設置爲True current_path = request.path_info for item in permission_list: pid = item["pid"] url = "^%s$" % item["url"] if re.match(url, current_path): if pid: temp_dict[pid]["active"] = True else: item["active"] = True # 將temp_dict轉換爲最終的menu_dict的數據格式 menu_dict = {} for item in temp_dict.values(): if item["menu_id"] in menu_dict: menu_dict[item["menu_id"]]["children"].append( {"title": item["title"], "url": item["url"], "active": item["active"]}) if item["active"]: menu_dict[item["menu_id"]]["active"] = True else: menu_dict[item["menu_id"]] = { "title": item["menu_name"], "active": item["active"], "children": [ {"title": item["title"], "url": item["url"], "active": item["active"]} ] } return {"menu_dict": menu_dict}
首先將須要展現的權限(parent_id爲None的)統一取到temp_dict字典中,而後根據用戶訪問的url修改權限的active值
最後將temp_dict轉換成咱們須要的結構,同時根據權限的active肯定菜單的active,這樣咱們就獲得了咱們須要的數據
可是,這裏咱們又發現一個問題,每次咱們訪問不一樣的url,到達不一樣的視圖函數時都須要這樣處理一遍數據,十分麻煩
咱們發現其實咱們訪問的每個頁面都有這樣的權限菜單,這就讓咱們想到了模板繼承和咱們以前用過的自定義標籤
from django import template import re register = template.Library() @register.inclusion_tag("menu.html") def get_menu(request): permission_list = request.session.get("permission_list") # 存儲全部放到菜單欄中的權限 temp_dict = {} for item in permission_list: pid = item["pid"] if not pid: item["active"] = False temp_dict[item["id"]] = item # 將須要標中的active設置爲True current_path = request.path_info for item in permission_list: pid = item["pid"] url = "^%s$" % item["url"] if re.match(url, current_path): if pid: temp_dict[pid]["active"] = True else: item["active"] = True # 將temp_dict轉換爲最終的menu_dict的數據格式 menu_dict = {} for item in temp_dict.values(): if item["menu_id"] in menu_dict: menu_dict[item["menu_id"]]["children"].append( {"title": item["title"], "url": item["url"], "active": item["active"]}) if item["active"]: menu_dict[item["menu_id"]]["active"] = True else: menu_dict[item["menu_id"]] = { "title": item["menu_name"], "active": item["active"], "children": [ {"title": item["title"], "url": item["url"], "active": item["active"]} ] } return {"menu_dict": menu_dict}
頁面模板
menu.html
<div class="menu"> {% for item in menu_dict.values %} <div class="item"> <div class="title"><a href="">{{ item.title }}</a></div> {% if item.active %} <div class="con"> {% else %} <div class="con hide"> {% endif %} {% for son in item.children %} {% if son.active %} <p><a href="{{ son.url }}" class="active">{{ son.title }}</a></p> {% else %} <p><a href="{{ son.url }}">{{ son.title }}</a></p> {% endif %} {% endfor %} </div> </div> {% endfor %} </div>
base.html
{% load my_tags %} <!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"> <title>Title</title> <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css"> <style> .header { width: 100%; height: 50px; background-color: #336699; } .menu, .content { float: left; } .menu { width: 200px; height: 600px; background-color: darkgray; } .hide { display: none; } .menu .title { font-size: 16px; color: #336699 !important; margin: 20px 0; } .con a { margin-left: 30px; color: white; } .active { color: red !important; } </style> </head> <body> <div class="header"></div> <div class="box"> <div class="row"> {% get_menu request %} <div class="content col-md-9"> {% block con %} {% endblock %} </div> </div> </div> </body> </html>
orders.html
{% extends "base.html" %} {% block con %} <h3>訂單列表</h3> <div class="col-md-6"> {% if per.add %} <p><a href="/orders/add/"><button class="btn btn-primary pull-right">添加訂單</button></a></p> {% endif %} <table class="table table-stripped"> <tr> <th>訂單號</th> <th>訂單日期</th> <th>商品名稱</th> <th>操做</th> </tr> <tr> <td>12343</td> <td>2012-12-12</td> <td>li</td> {% if per.delete %} <td><a href=""><button class="btn btn-danger btn-sm">刪除</button></a></td> {% endif %} </tr> </table> </div> {% endblock %}
其它的頁面也都繼承該模板
最後的視圖函數
from django.shortcuts import render, redirect, HttpResponse from rbac import models from rbac.service.base import * import re # Create your views here. def login(request): if request.method == "GET": return render(request, "login.html") else: user = request.POST.get("user") pwd = request.POST.get("pwd") user = models.UserInfo.objects.filter(name=user, pwd=pwd).first() if user: # 驗證成功以後 request.session["user_id"] = user.pk # 當前登陸用戶的全部權限 from rbac.service.initial import initial_session initial_session(request, user) return HttpResponse("登陸成功") else: return redirect("/login/") class UserPermissions(Permissions): def xxx(self): return "xxx" in self.code_list def users(request): return render(request, "users.html", locals()) def orders(request): permission_dict = request.session.get("permission_dict") permission_codes = request.permission_codes per = Permissions(permission_codes) return render(request, "orders.html", locals()) def orders_add(request): return HttpResponse("添加訂單") def m1(request): return render(request, "m1.html") def m2(request): return render(request, "m2.html")
這樣訪問/orders/時,咱們看到簡單的效果了