1. 問:為什么程序需要權限控制?
答:生活中的權限限制,① 看災難片電影《2012》中富人和權貴有權登上諾亞方舟,窮苦老百姓只有等着災難的來臨;② 屌絲們,有沒有想過為什么那些長得漂亮身材好的姑娘在你身邊不存在呢?因為有錢人和漂亮姑娘都是珍貴稀有的,稀有的人在一起玩耍和解鎖各種姿勢。而你,無權擁有他們,只能自己玩自己了。
程序開發時的權限控制,對於不同用戶使用系統時候就應該有不同的功能,如:
- 普通員工
- 部門主管
- 總監
- 總裁
所以,只要有不同角色的人員來使用系統,那么就肯定需要權限系統。
2. 問:為什么要開發權限組件?
答:假設你今年25歲,從今天開始寫代碼到80歲,每年寫5個項目,那么你的一生就會寫275個項目,保守估計其中應該有150+個都需要用到權限控制,為了以后不再重復的寫代碼,所以就開發一個權限組件以便之后55年的歲月中使用。 親,不要太較真哦,你覺得程序員能到80歲么,哈哈哈哈哈哈哈
偷偷告訴你:老程序員開發速度快,其中一個原因是經驗豐富,另外一個就是他自己保留了很多組件,新系統開發時,只需把組件拼湊起來基本就可以完成。
3. 問:web開發中權限指的是什么?
答:web程序是通過 url 的切換來查看不同的頁面(功能),所以權限指的其實就是URL,對url控制就是對權限的控制。
結論:一個人有多少個權限就取決於他有多少個URL的訪問權限。
權限表結構設計:第一版
問答環節中已得出權限就是URL的結論,那么就可以開始設計表結構了。
- 一個用戶可以有多個權限。
- 一個權限可以分配給多個用戶。
你設計的表結構大概會是這個樣子:



現在,此時此刻是不是覺得自己設計出的表結構棒棒噠!!!
But,無論是是否承認,你還是too young too native,因為老漢腚眼一看就有問題....
問題:假設 “老男孩”和“Alex” 這倆貨都是老板,老板的權限一定是非常多。那么試想,如果給這倆貨分配權限時需要在【用戶權限關系表中】添加好多條數據。假設再次需要對老板的權限進行修改時,又需要在【用戶權限關系表】中找到這倆人所有的數據進行更新,太他媽煩了吧!!! 類似的,如果給其他相同角色的人來分配權限時,必然會非常繁瑣。
權限表結構設計:第二版
聰明機智的一定在上述的表述中看出了寫門道,如果對用戶進行角色的划分,然后對角色進行權限的分配,這不就迎刃而解了么。
- 一個人可以有多個角色。
- 一個角色可以有多個人。
- 一個角色可以有多個權限。
- 一個權限可以分配給多個角色。
表結構設計:





這次調整之后,由原來的【基於用戶的權限控制】轉換成【基於角色的權限控制】,以后再進行分配權限時只需要給指定角色分配一次權限,給眾多用戶再次分配指定角色即可。
from django.db import models class Permission(models.Model): """ 權限表 """ title = models.CharField(verbose_name='標題', max_length=32) url = models.CharField(verbose_name='含正則的URL', max_length=128) def __str__(self): return self.title class Role(models.Model): """ 角色 """ title = models.CharField(verbose_name='角色名稱', max_length=32) permissions = models.ManyToManyField(verbose_name='擁有的所有權限', to='Permission', blank=True) def __str__(self): return self.title class UserInfo(models.Model): """ 用戶表 """ name = 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) def __str__(self): return self.name
小伙子,告訴你一個事實,不經意間,你居然設計出了一個經典的權限訪問控制系統:rbac(Role-Based Access Control)基於角色的權限訪問控制。你這么優秀,為什么不來老男孩IT教育?路飛學城也行呀! 哈哈哈哈。
注意:現在的設計還不是最終版,但之后的設計都是在此版本基礎上擴增的,為了讓大家能夠更好的理解,我們暫且再此基礎上繼續開發,直到遇到無法滿足的情況,再進行整改。
源碼示例:猛擊下載
客戶管理之權限控制
學習知識最好的方式就是試錯,坑踩多了那么學到的知識自然而然就多了,所以接下里下來我們用《客戶管理》系統為示例,提出功能並實現,並且隨着功能越來越多,一點點來找出問題,並解決問題。
目錄結構:
luffy_permission/
├── db.sqlite3
├── luffy_permission
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── rbac # 權限組件,便於以后應用到其他系統
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── templates
└── web # 客戶管理業務
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── tests.py
└── views.py
from django.db import models class Permission(models.Model): """ 權限表 """ title = models.CharField(verbose_name='標題', max_length=32) url = models.CharField(verbose_name='含正則的URL', max_length=128) def __str__(self): return self.title class Role(models.Model): """ 角色 """ title = models.CharField(verbose_name='角色名稱', max_length=32) permissions = models.ManyToManyField(verbose_name='擁有的所有權限', to='Permission', blank=True) def __str__(self): return self.title class UserInfo(models.Model): """ 用戶表 """ name = 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) def __str__(self): return self.name
from django.db import models class Customer(models.Model): """ 客戶表 """ name = models.CharField(verbose_name='姓名', max_length=32) age = models.CharField(verbose_name='年齡', max_length=32) email = models.EmailField(verbose_name='郵箱', max_length=32) company = models.CharField(verbose_name='公司', max_length=32) class Payment(models.Model): """ 付費記錄 """ customer = models.ForeignKey(verbose_name='關聯客戶', to='Customer') money = models.IntegerField(verbose_name='付費金額') create_time = models.DateTimeField(verbose_name='付費時間', auto_now_add=True)
《客戶管理》系統截圖:基本增刪改查和Excel導入源碼下載:猛擊這里



以上簡易版客戶管理系統中的URL有:
- 客戶管理
- 客戶列表:/customer/list/
- 添加客戶:/customer/add/
- 刪除客戶:/customer/list/(?P<cid>\d+)/
- 修改客戶:/customer/edit/(?P<cid>\d+)/
- 批量導入:/customer/import/
- 下載模板:/customer/tpl/
- 賬單管理
- 賬單列表:/payment/list/
- 添加賬單:/payment/add/
- 刪除賬單:/payment/del/(?P<pid>\d+)/
- 修改賬單:/payment/edit/<?P<pid>\d+/
那么接下來,我們就在權限組件中錄入相關信息:
- 錄入權限
- 創建用戶
- 創建角色
- 用戶分配角色
- 角色分配權限
這么一來,用戶登錄時,就可以根據自己的【用戶】找到所有的角色,再根據角色找到所有的權限,再將權限信息放入session,以后每次訪問時候需要先去session檢查是否有權訪問。
已錄入權限數據源碼下載:猛擊這里
含用戶登錄權限源碼下載:猛擊這里(簡易版)
含用戶登錄權限源碼下載:猛擊這里
至此,基本的權限控制已經完成,基本流程為:
- 用戶登錄,獲取權限信息並放入session
- 用戶訪問,在中間件從session中獲取用戶權限信息,並進行權限驗證。
所有示例中的賬戶信息:
賬戶一:
用戶名:alex
密碼:123
賬戶二:
用戶名:wupeiqi
密碼:123
客戶管理之動態“一級”菜單
上述過程中的菜單是在程序中定義好的,無法根據用戶權限進行動態配置。
那么,接下來我們來完成一個 “單級菜單”的功能:
from django.db import models class Permission(models.Model): """ 權限表 """ title = models.CharField(verbose_name='標題', max_length=32) url = models.CharField(verbose_name='含正則的URL', max_length=128) icon = models.CharField(verbose_name='圖標', max_length=32, null=True, blank=True, help_text='菜單才設置圖標') is_menu = models.BooleanField(verbose_name='是否是菜單', default=False) def __str__(self): return self.title class Role(models.Model): """ 角色 """ title = models.CharField(verbose_name='角色名稱', max_length=32) permissions = models.ManyToManyField(verbose_name='擁有的所有權限', to='Permission', blank=True) def __str__(self): return self.title class UserInfo(models.Model): """ 用戶表 """ name = 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) def __str__(self): return self.name
這樣可以開發出一個單級菜單的示例:


示例源碼下載:猛擊這里(無默認選中)
示例源碼下載:猛擊這里(含默認選中)
客戶管理之動態“二級”菜單
對於功能比較少的應用程序 “一級菜單” 基本可以滿足需求,但是功能多的程序就需要 “二級菜單” 了,並且訪問時候需要默認選中指定菜單。
from django.db import models class Menu(models.Model): """ 菜單 """ title = models.CharField(verbose_name='菜單', max_length=32) icon = models.CharField(verbose_name='圖標', max_length=32) def __str__(self): return self.title class Permission(models.Model): """ 權限表 """ title = models.CharField(verbose_name='標題', max_length=32) url = models.CharField(verbose_name='含正則的URL', max_length=128) menu = models.ForeignKey(verbose_name='菜單', to='Menu', null=True, blank=True, help_text='null表示非菜單') def __str__(self): return self.title class Role(models.Model): """ 角色 """ title = models.CharField(verbose_name='角色名稱', max_length=32) permissions = models.ManyToManyField(verbose_name='擁有的所有權限', to='Permission', blank=True) def __str__(self): return self.title class UserInfo(models.Model): """ 用戶表 """ name = 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) def __str__(self): return self.name
示例效果:

示例源碼下載:猛擊這里
示例源碼下載:猛擊這里(路飛線上錄制代碼示例)
客戶管理之默認展開非菜單URL
由於很多URL都是不能作為菜單,所以當點擊該類功能時,是無法默認展開菜單的,如:
- 刪除
- 修改
- ...


此類頁面只能通過菜單頁面二次點擊才能跳轉,此時也應該為他們默認展開原菜單。
from django.db import models class Menu(models.Model): """ 菜單 """ title = models.CharField(verbose_name='菜單', max_length=32) icon = models.CharField(verbose_name='圖標', max_length=32) def __str__(self): return self.title class Permission(models.Model): """ 權限表 """ title = models.CharField(verbose_name='標題', max_length=32) url = models.CharField(verbose_name='含正則的URL', max_length=128) pid = models.ForeignKey(verbose_name='默認選中權限', to='Permission', related_name='ps', null=True, blank=True, help_text="對於無法作為菜單的URL,可以為其選擇一個可以作為菜單的權限,那么訪問時,則默認選中此權限", limit_choices_to={'menu__isnull': False}) menu = models.ForeignKey(verbose_name='菜單', to='Menu', null=True, blank=True, help_text='null表示非菜單') def __str__(self): return self.title class Role(models.Model): """ 角色 """ title = models.CharField(verbose_name='角色名稱', max_length=32) permissions = models.ManyToManyField(verbose_name='擁有的所有權限', to='Permission', blank=True) def __str__(self): return self.title class UserInfo(models.Model): """ 用戶表 """ name = 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) def __str__(self): return self.name
功能完成后的示例如下:

示例源碼下載:猛擊這里
示例源碼下載:猛擊這里(路飛線上錄制代碼示例)
客戶管理之訪問路徑導航
如果想要保留放的地址,那么就可以通過在權限配置中獲取此功能,示例如下:


示例源碼下載:猛擊這里
客戶管理之 權限粒度控制按鈕級別
不同用戶登錄系統時候,根據權限不同來控制是否限制指定按鈕,如:


示例源碼下載:猛擊這里
示例源碼下載:猛擊這里(路飛線上錄制代碼示例)
客戶管理之 編輯權限和分配權限
給用戶進行權限的分配。
1. 用戶和角色管理
示例源碼下載:猛擊這里(用戶和角色管理)
2. 一級菜單


示例源碼下載:猛擊這里(菜單和權限管理之一級菜單)
3. 二級菜單管理

示例源碼下載:猛擊這里(菜單和權限管理之二級菜單)
4. 三級菜單管理(權限管理)

示例源碼下載:猛擊這里(菜單和權限管理之三級菜單)
5. django formset示例

源碼示例下載:猛擊這里(django formset實現批量添加和編輯)
6. 批量操作權限的顯示

源碼示例下載:猛擊這里(批量操作權限的顯示)
7. 批量操作權限:添加、刪除、更新
源碼示例下載:猛擊這里(權限的批量增刪改)
8. 權限分配

源碼示例下載:猛擊這里(權限分配)
RBAC組件應用及文檔
"""
1. 將rbac組件拷貝項目。
2. 將rbac/migrations目錄中的數據庫遷移記錄刪除
3. 業務系統中用戶表結構的設計
業務表結構中的用戶表需要和rbac中的用戶有繼承關系,如:
rbac/models.py
class UserInfo(models.Model):
# 用戶表
name = 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) 嚴重提醒 Role 不要加引號
def __str__(self):
return self.name
class Meta:
# django以后再做數據庫遷移時,不再為UserInfo類創建相關的表以及表結構了。
# 此類可以當做"父類",被其他Model類繼承。
abstract = True
業務/models.py
class UserInfo(RbacUserInfo):
phone = models.CharField(verbose_name='聯系方式', max_length=32)
level_choices = (
(1, 'T1'),
(2, 'T2'),
(3, 'T3'),
)
level = models.IntegerField(verbose_name='級別', choices=level_choices)
depart = models.ForeignKey(verbose_name='部門', to='Department')
4. 講業務系統中的用戶表的路徑寫到配置文件。
# 業務中的用戶表
RBAC_USER_MODLE_CLASS = "app01.models.UserInfo"
用於在rbac分配權限時,讀取業務表中的用戶信息。
5. 業務邏輯開發
將所有的路由都設置一個name,如:
url(r'^login/$', account.login, name='login'),
url(r'^logout/$', account.logout, name='logout'),
url(r'^index/$', account.index, name='index'),
url(r'^user/list/$', user.user_list, name='user_list'),
url(r'^user/add/$', user.user_add, name='user_add'),
url(r'^user/edit/(?P<pk>\d+)/$', user.user_edit, name='user_edit'),
url(r'^user/del/(?P<pk>\d+)/$', user.user_del, name='user_del'),
url(r'^user/reset/password/(?P<pk>\d+)/$', user.user_reset_pwd, name='user_reset_pwd'),
url(r'^host/list/$', host.host_list, name='host_list'),
url(r'^host/add/$', host.host_add, name='host_add'),
url(r'^host/edit/(?P<pk>\d+)/$', host.host_edit, name='host_edit'),
url(r'^host/del/(?P<pk>\d+)/$', host.host_del, name='host_del'),
用於反向生成URL以及粒度控制到按鈕級別的權限控制。
6. 權限信息錄入
- 在url中添加rbac的路由分發,注意:必須設置namespace
urlpatterns = [
...
url(r'^rbac/', include('rbac.urls', namespace='rbac')),
]
- rbac提供的地址進行操作
- http://127.0.0.1:8000/rbac/menu/list/
- http://127.0.0.1:8000/rbac/role/list/
- http://127.0.0.1:8000/rbac/distribute/permissions/
相關配置:自動發現URL時,排除的URL:
# 自動化發現路由中URL時,排除的URL
AUTO_DISCOVER_EXCLUDE = [
'/admin/.*',
'/login/',
'/logout/',
'/index/',
]
7. 編寫用戶登錄的邏輯【進行權限初始化】
from django.shortcuts import render, redirect
from app01 import models
from rbac.service.init_permission import init_permission
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
user = request.POST.get('username')
pwd = request.POST.get('password')
user_object = models.UserInfo.objects.filter(name=user, password=pwd).first()
if not user_object:
return render(request, 'login.html', {'error': '用戶名或密碼錯誤'})
# 用戶權限信息的初始化
init_permission(user_object, request)
return redirect('/index/')
相關配置: 權限和菜單的session key:
setting.py
PERMISSION_SESSION_KEY = "luffy_permission_url_list_key"
MENU_SESSION_KEY = "luffy_permission_menu_key"
8. 編寫一個首頁的邏輯
def index(request):
return render(request, 'index.html')
相關配置:需要登錄但無需權限的URL
# 需要登錄但無需權限的URL
NO_PERMISSION_LIST = [
'/index/',
'/logout/',
]
9. 通過中間件進行權限校驗
# 權限校驗
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'rbac.middlewares.rbac.RbacMiddleware',
]
# 白名單,無需登錄就可以訪問
VALID_URL_LIST = [
'/login/',
'/admin/.*'
]
10. 粒度到按鈕級別的控制
{% extends 'layout.html' %}
{% load rbac %}
{% block content %}
<div class="luffy-container">
<div class="btn-group" style="margin: 5px 0">
{% if request|has_permission:'host_add' %}
<a class="btn btn-default" href="{% memory_url request 'host_add' %}">
<i class="fa fa-plus-square" aria-hidden="true"></i> 添加主機
</a>
{% endif %}
</div>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>主機名</th>
<th>IP</th>
<th>部門</th>
{% if request|has_permission:'host_edit' or request|has_permission:'host_del' %}
<th>操作</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for row in host_queryset %}
<tr>
<td>{{ row.hostname }}</td>
<td>{{ row.ip }}</td>
<td>{{ row.depart.title }}</td>
{% if request|has_permission:'host_edit' or request|has_permission:'host_del' %}
<td>
{% if request|has_permission:'host_edit' %}
<a style="color: #333333;" href="{% memory_url request 'host_edit' pk=row.id %}">
<i class="fa fa-edit" aria-hidden="true"></i></a>
{% endif %}
{% if request|has_permission:'host_del' %}
<a style="color: #d9534f;" href="{% memory_url request 'host_del' pk=row.id %}"><i
class="fa fa-trash-o"></i></a>
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
總結,目的是希望在任意系統中應用權限系統。
- 用戶登錄 + 用戶首頁 + 用戶注銷 業務邏輯
- 項目業務邏輯開發
注意:開發時候靈活的去設置layout.html中的兩個inclusion_tag
<div class="pg-body">
<div class="left-menu">
<div class="menu-body">
{% multi_menu request %} # 開發時,去掉;上下時,取回。
</div>
</div>
<div class="right-body">
<div>
{% breadcrumb request %} # 開發時,去掉;上下時,取回。
</div>
{% block content %} {% endblock %}
</div>
</div>
- 權限信息的錄入
- 配置文件
# 注冊APP
INSTALLED_APPS = [
...
'rbac.apps.RbacConfig'
]
# 應用中間件
MIDDLEWARE = [
...
'rbac.middlewares.rbac.RbacMiddleware',
]
# 業務中的用戶表
RBAC_USER_MODLE_CLASS = "app01.models.UserInfo"
# 權限在Session中存儲的key
PERMISSION_SESSION_KEY = "luffy_permission_url_list_key"
# 菜單在Session中存儲的key
MENU_SESSION_KEY = "luffy_permission_menu_key"
# 白名單
VALID_URL_LIST = [
'/login/',
'/admin/.*'
]
# 需要登錄但無需權限的URL
NO_PERMISSION_LIST = [
'/index/',
'/logout/',
]
# 自動化發現路由中URL時,排除的URL
AUTO_DISCOVER_EXCLUDE = [
'/admin/.*',
'/login/',
'/logout/',
'/index/',
]
- 粒度到按鈕級別的控制
"""
最終版組件下載:rbac.zip
源碼示例下載:rbac組件應用之主機管理系統【auto_luffy.zip】

