基於角色的權限管理
權限組件
在我們寫項目時,可能會遇到給不同的用戶分配不同的權限的情況,那么什么是權限呢?權限其實就是一個url
不同的url代表不同的功能,限定用戶能訪問的url,就給了用戶不同的權限
權限管理在很多項目中都有用到,所以我們可以講權限管理的邏輯寫成一個組件
使它在不同的項目中只要經過一定的修改就能使用
創建項目
在一個項目中可以包含多個組件,同樣一個組件也可以用於多個項目,這里我們想創建一個項目
然后創建兩個app,一個app01寫項目的主邏輯,一個rbac(Role-Based Access Control)寫權限相關的邏輯

建表
我們首先能想到的是用戶和權限表,一個用戶可以有多個權限,而一個權限也可以對應多個用戶,這樣他們就是多對多的關系
但是這樣的話,我們會發現一個問題,比如一個公司有很多銷售員,這些銷售員都有同樣的權限,這時我們就需要添加很多重復的信息
這里我們可以再添加一張角色表,讓角色和用戶、權限都有多對多的關系
這樣有新的用戶后,只要給該用戶分配一個角色就行了,而該角色又擁有相應的權限,就不需要再添加重復的信息了
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(1、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/時,我們看到簡單的效果了


