Django之rbac應用組件
一、權限管理的訪問控制
權限管理,一般指控制用戶的訪問權限,使得用戶可以訪問而且只能訪問自己被授權的資源,不能多也不能少。現在的軟件系統里基本上都用到了權限管理,只是控制的粒度、層面和側重點會有所不同,比較完善的權限管理包括四個方面的訪問控制:
1.功能(最基礎):以用戶完成某一功能為准。如“添加用戶”、“刪除用戶”
2.數據:比功能訪問權限的控制粒度更小,如“管理員可看到比一般用戶更多的信息”
3.時間:給訪問權限添加時間控制,讓訪問的資源在某一時間段中可用。如”12306只能在7:00-23:00時間段內購票“
4.空間:給訪問權限添加空間控制,根據訪問用戶的空間位置不同,而對用戶的訪問資源進行限制。如”很多人都在問,為什么在中國上不了facebook……“
二、設計理念
權限管理的設計理念有很多,像ABAC(基於屬性的訪問控制)、ACL(基於資源的訪問控制)、RBAC(基於資源的訪問控制)、GBAC(基於組的訪問控制)等等,它們各有利弊,現在最常用的是RBAC,理論較完善。
1.基於資源的權限控制——ACL
ACL(Access Control List)訪問控制列表,是最早也是最基本的一種訪問控制機制,它的權限控制是圍繞”資源“展開的,即每一項資源,都配有一個列表,這個列表記錄的是哪些用戶可以對這些資源進行哪些操作
這種訪問控制非常簡單,只要把用戶和資源連接起來就行了,但是當用戶和資源增多時,就會產生大量的訪問權限列表,管理這些訪問控制列表本身就是一件非常繁重的工作,這樣便使得ACL在性能上無法勝任實際應用,所以說性能是硬傷
2.基於角色的權限控制——RBAC
RBAC(Role-Based Access Control)基於角色的訪問控制,在這種訪問控制機制中,引入了Role的概念,將用戶與權限之間的關系進行了解耦,讓用戶通過角色與權限進行關聯。在RBAC模型中,who、what、how構成了訪問權限三元組,即”who對what進行how的操作“。
一個用戶可有多種角色,每一種角色擁有多個權限,在用戶與角色、角色與權限之間,都是多對多的關系。通過給用戶分配角色,使得用戶擁有對系統的部分使用權限。在實際設計的過程中,可讓角色和資源直接進行綁定,權限控制體現在角色與資源的關聯上
角色,是一定數量的權限集合,也可看成是對擁有相同角色的用戶進行的分類。
3.引入”組“概念的權限控制——RBAC
這種方案是比較簡單的權限管理,一般情況下這樣的設計已經足夠了,但是如果要給一組用戶直接分配權限的話就有問題了,所以又引入了用戶組的概念。
用戶組,是一組用戶的集合,一個用戶組擁有多個權限。
通過用戶組的啟發,其實我們也可以增加角色組、資源組等等各種組,實現相應的繼承功能。不過這樣就有點繁瑣了,還是用的時候根據實際需求權衡吧。
三、基於RBAC的權限管理
在上邊的分析中,得出了一種包含用戶、角色、權限、組等幾個主體的權限管理,它們之間的關聯都是多對多的,由此得到的ER圖如下:
RBAC權限管理模型在加入”組“概念后,在實現繼承功能的基礎上,更加靈活的適應了需求的變更。它主要的配置為:用戶-角色配置、用戶-用戶組配置,角色-權限配置,用戶組-資源配置,這些配置對應到數據庫中就是兩個主表之間的第三張表,里邊存儲的是用戶操作的記錄,服務於主表以供查詢。
四、Pyhton中RBAC的設計思路
1、數據庫層面(models)
用戶、角色、權限、權限組、菜單(菜單只是為了在頁面展示以及菜單作用)
2、中間件層(middlewares)
中間件層是在用戶請求服務器最前面的一層過濾系統,在rbac組件中它的作用是:
a、讓未登錄的用戶無法訪問相應的URL地址。 (用戶登錄之后 才能擁有特定權限,並且把相關權限格式化成字典格式 存入session)
b、把當前登錄的用戶權限和當前URL匹配 是否有權限,如果沒有就返回404。
3、view視圖層(view)
處理:路由系統分配的請求。
4、HTML層(前端頁面顯示)
前端顯示頁面(users.html)頁面的時候,繼承了模板頁面(extends "layout.html" ),頁面“ layout.html ”導入了{% load rbac %}。在rbac組件中templatetags文件下的rbac.py:@register.inclusion_tag。
在templatetags文件下的rbac.py文件內容中已經把用戶相關權限格式化成menu_result,渲染到了rbac下面的menu.html文件里面。在menu.html里面已經根據code判斷是是否顯示相關的權限。
整個流程如下圖:
五、Python實現代碼:
代碼目錄:

import re from django.conf import settings from django.shortcuts import HttpResponse,render 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): # 當前訪問的URL current_url = request.path_info # print("type(current_url):",type(current_url)) # print("type(current_url):",type(current_url.split("/")[3])) # if "/favicon.ico"==request.path_info : # return HttpResponse("404") for valid in settings.VALID_LIST: if re.match(valid,current_url): return None # 當前用戶的所有權限 permission_dict = request.session.get(settings.PERMISSION_DICT_SESSION_KEY) print("permission_dict:",permission_dict) if not permission_dict: # return HttpResponse('當前用戶無權限信息') return HttpResponse('當前用戶未登錄!') # 用戶權限和當前URL進行匹配 flag = False for item in permission_dict.values(): urls = item['urls'] codes = item['codes'] for rex in urls: reg = settings.REX_FORMAT %(rex,) print(rex,current_url) if re.match(reg,current_url): flag = True request.permission_codes = codes break if flag: break if not flag: # return HttpResponse('無權限訪問,請聯系管理員。') return render(request,"404.html")

# -*- coding: utf-8 -*- # Generated by Django 1.11.6 on 2018-01-09 06:36 from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='Department', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=32)), ], ), migrations.CreateModel( name='Host', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('hostname', models.CharField(max_length=32, verbose_name='主機名')), ('ip', models.CharField(max_length=32, verbose_name='IP')), ('port', models.IntegerField(verbose_name='端口')), ('dp', models.ManyToManyField(to='rbac.Department', verbose_name='部門')), ], ), migrations.CreateModel( name='Menu', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=32)), ], ), migrations.CreateModel( name='Permission', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=32, verbose_name='權限名稱')), ('url', models.CharField(max_length=255, verbose_name='含正則的URL')), ('code', models.CharField(max_length=32, verbose_name='權限代碼')), ], ), migrations.CreateModel( name='PermissionGroup', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('caption', models.CharField(max_length=32)), ('menu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rbac.Menu', verbose_name='所屬菜單')), ], ), migrations.CreateModel( name='Role', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=32, verbose_name='角色名稱')), ('permissions', models.ManyToManyField(to='rbac.Permission', verbose_name='擁有權限')), ], ), migrations.CreateModel( name='UserInfo', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('username', models.CharField(max_length=32, verbose_name='用戶名')), ('password', models.CharField(max_length=64, verbose_name='密碼')), ('roles', models.ManyToManyField(to='rbac.Role', verbose_name='擁有角色')), ], ), migrations.AddField( model_name='permission', name='group', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rbac.PermissionGroup', verbose_name='所屬權限組'), ), migrations.AddField( model_name='permission', name='group_menu', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='xxx', to='rbac.Permission', verbose_name='組內菜單'), ), migrations.AddField( model_name='host', name='user', field=models.ManyToManyField(default=1, to='rbac.UserInfo', verbose_name='用戶名'), ), ]

from django.conf import settings def init_permission(user,request): """ 用於做用戶登錄成功之后,權限信息的初始化。 :param user: 登錄的用戶對象 :param request: 請求相關的對象 :return: """ """ [ {'permissions__title': '用戶列表', 'permissions__url': '/users/', 'permissions__code': 'list', 'permissions__group_id': 1} {'permissions__title': '添加用戶', 'permissions__url': '/users/add/', 'permissions__code': 'add', 'permissions__group_id': 1} {'permissions__title': '刪除用戶', 'permissions__url': '/users/del/(\\d+)/', 'permissions__code': 'del', 'permissions__group_id': 1} {'permissions__title': '修改用戶', 'permissions__url': '/users/edit/(\\d+)/', 'permissions__code': 'edit', 'permissions__group_id': 1} {'permissions__title': '主機列表', 'permissions__url': '/hosts/', 'permissions__code': 'list', 'permissions__group_id': 2} {'permissions__title': '添加主機', 'permissions__url': '/hosts/add/', 'permissions__code': 'add', 'permissions__group_id': 2} {'permissions__title': '刪除主機', 'permissions__url': '/hosts/del/(\\d+)/', 'permissions__code': 'del', 'permissions__group_id': 2} {'permissions__title': '修改主機', 'permissions__url': '/hosts/edit/(\\d+)/', 'permissions__code': 'edit', 'permissions__group_id': 2} ] { 1(權限組ID): { urls: [/u sers/,/users/add/ ,/users/del/(\d+)/], codes: [list,add,del] }, 2: { urls: [/hosts/,/hosts/add/ ,/hosts/del/(\d+)/], codes: [list,add,del] } } """ permission_list = user.roles.filter(permissions__id__isnull=False).values( 'permissions__id', # 權限ID 'permissions__title', # 權限名稱 'permissions__url', # 權限URL 'permissions__code', # 權限CODE 'permissions__group_menu_id', # 組內菜單ID(null表示自己是菜單,1) 'permissions__group_id', # 權限組ID 'permissions__group__menu__id', # 一級菜單ID 'permissions__group__menu__name', # 一級菜單名稱 ).distinct() # 獲取權限信息+組+菜單,放入session,用於以后在頁面上自動生成動態菜單。 permission_memu_list = [] for item in permission_list: val = { 'id':item['permissions__id'], 'title':item['permissions__title'], 'url':item['permissions__url'], 'pid':item['permissions__group_menu_id'], 'menu_id':item['permissions__group__menu__id'], 'menu__name':item['permissions__group__menu__name'], } permission_memu_list.append(val) request.session[settings.PERMISSION_MENU_SESSION_KEY] = permission_memu_list # 獲取權限信息,放入session,用於以后在中間件中權限進行匹配 permission_dict = {} for permission in permission_list: group_id = permission['permissions__group_id'] url = permission['permissions__url'] code = permission['permissions__code'] if group_id in permission_dict: permission_dict[group_id]['urls'].append(url) permission_dict[group_id]['codes'].append(code) else: permission_dict[group_id] = {'urls': [url, ], 'codes': [code, ]} request.session[settings.PERMISSION_DICT_SESSION_KEY] = permission_dict

.menu-item .menu-title{ height: 30px; background-color: cornflowerblue; } .menu-item .menu-content{ margin-left: 20px; } .menu-item .menu-content a{ display: block; } .menu-item .menu-content a.active{ color: red; } .hide{ display: none; }

{% for menu in menu_result.values %} <div class="menu-item"> <div class="menu-title">{{ menu.menu__name }}</div> {# {% if menu.active %}#} {# <div class="menu-content">#} {# {% else %}#} {# <div class="menu-content hide">#} {# {% endif %}#} <div class="menu-content"> {% for per in menu.children %} {% if per.active %} <a href="{{ per.url }}" class="active">{{ per.title }}</a> {% else %} <a href="{{ per.url }}">{{ per.title }}</a> {% endif %} {% endfor %} </div> </div> {% endfor %}

import re from django.template import Library from django.conf import settings register = Library() """ {% menu request %} """ @register.inclusion_tag('rbac/menu.html') def menu(request): current_url = request.path_info # 獲取session中菜單信息,自動生成二級菜單【默認選中,默認展開】 permission_menu_list = request.session.get(settings.PERMISSION_MENU_SESSION_KEY) per_dict = {} for item in permission_menu_list: if not item['pid']: per_dict[item['id']] = item for item in permission_menu_list: reg = settings.REX_FORMAT % (item['url']) if not re.match(reg, current_url): continue # 匹配成功 if item['pid']: per_dict[item['pid']]['active'] = True else: item['active'] = True """ { 1: {'id': 1, 'title': '用戶列表', 'url': '/users/', 'pid': None, 'menu_id': 1, 'menu__name': '菜單1', 'active': True}, 5: {'id': 5, 'title': '主機列表', 'url': '/hosts/', 'pid': None, 'menu_id': 1, 'menu__name': '菜單1'} 10: {'id': 10, 'title': 'xx列表', 'url': '/hosts/', 'pid': None, 'menu_id': 2, 'menu__name': '菜單2'} } { 1:{ 'menu__name': '菜單1', 'active': True, 'children':[ {'id': 1, 'title': '用戶列表', 'url': '/users/','active': True} {'id': 5, 'title': '主機列表', 'url': '/users/'} ] }, 2:{ 'menu__name': '菜單1', 'children':[ {'id': 10, 'title': 'xx列表', 'url': '/hosts/'} ] } } """ menu_result = {} for item in per_dict.values(): menu_id = item['menu_id'] if menu_id in menu_result: temp = {'id': item['id'], 'title': item['title'], 'url': item['url'], 'active': item.get('active', False)} menu_result[menu_id]['children'].append(temp) if item.get('active', False): menu_result[menu_id]['active'] = item.get('active', False) else: menu_result[menu_id] = { 'menu__name': item['menu__name'], 'active': item.get('active', False), 'children': [ {'id': item['id'], 'title': item['title'], 'url': item['url'], 'active': item.get('active', False)} ] } return {'menu_result':menu_result}

# -*- coding: utf-8 -*- __author__ = 'ShengLeQi' from django.forms import Form,ModelForm from django.forms import fields from django.forms import widgets from rbac import models from django import forms from django.core.exceptions import NON_FIELD_ERRORS,ValidationError class LoginForm(Form): username=fields.CharField( label="用戶名", required=True, error_messages={ 'required':'用戶名不能為空', }, widget=widgets.TextInput(attrs={'class':'form-control'}) ) password=fields.CharField( label='密碼', required=True, error_messages={ 'required': '密碼不能為空' }, widget=widgets.PasswordInput(attrs={'class':'form-control'}) ) class HostModelsForm(ModelForm): class Meta: model=models.Host fields=("hostname","ip","port","user","dp") widgets = { 'hostname': widgets.TextInput(attrs={"class": "form-control"}), 'ip': widgets.TextInput(attrs={"class": "form-control"}), 'port': widgets.NumberInput(attrs={"class": "form-control"}), 'user': widgets.SelectMultiple(attrs={"class": "form-control"}), 'dp': widgets.SelectMultiple(attrs={"class": "form-control"}), } labels={ "ip":"IP", "port":"端口", } error_messages={ "ip":{ "required":"IP不能為空", } } class RegForm(Form): username=forms.CharField(label="用戶名", min_length=3, widget=widgets.TextInput(attrs={"class": "form-control"}) ) password=forms.CharField(label="密碼", min_length=3, widget=widgets.PasswordInput(attrs={"class": "form-control"}) ) re_password=forms.CharField(label="確認密碼", min_length=3, widget=widgets.PasswordInput(attrs={"class": "form-control"}) ) email=forms.EmailField(label="郵箱", min_length=3, widget=widgets.TextInput(attrs={"class": "form-control"}) ) def clean_username(self): return self.cleaned_data.get("username") def clean(self): if self.cleaned_data.get("password")==self.cleaned_data.get("re_password"): return self.cleaned_data else: raise ValidationError("兩次密碼不一致") class UserInfoModelsForm(ModelForm): class Meta: model=models.UserInfo fields=("username","password","roles") widgets = { 'username': widgets.TextInput(attrs={"class": "form-control"}), 'password': widgets.PasswordInput(attrs={"class": "form-control"}), 'roles': widgets.SelectMultiple(attrs={"class": "form-control"}), } labels={ "username":"用戶名", "password":"密碼", "roles":"角色", } error_messages={ "ip":{ "required":"IP不能為空", } }

from django.db import models class UserInfo(models.Model): """ 用戶表 1 alex 123 2 tianle 123 2 yanglei 123 """ username = models.CharField(verbose_name='用戶名',max_length=32) password = models.CharField(verbose_name='密碼',max_length=64) roles = models.ManyToManyField(verbose_name='擁有角色',to="Role") def __str__(self): return self.username class Role(models.Model): """ 角色表 1 CEO 2 CTO 3 UFO 4 銷售主管 5 銷售員 """ title = models.CharField(verbose_name='角色名稱',max_length=32) permissions = models.ManyToManyField(verbose_name='擁有權限',to="Permission") def __str__(self): return self.title class Menu(models.Model): """ 菜單表 菜單1: 用戶權限組 用戶列表 主機權限組 主機列表 """ name = models.CharField(max_length=32) def __str__(self): return self.name class PermissionGroup(models.Model): """ 權限組 1 用戶權限組 用戶列表 2 主機權限組 主機列表 """ caption = models.CharField(max_length=32) menu = models.ForeignKey(verbose_name='所屬菜單',to='Menu') class Permission(models.Model): """ 權限表 組內菜單ID 1 用戶列表 /users/ list 1 null 2 添加用戶 /users/add/ add 1 1 3 刪除用戶 /users/del/(\d+)/ del 1 1 4 修改用戶 /users/edit/(\d+)/ edit 1 1 5 主機列表 /hosts/ list 2 null 6 添加主機 /hosts/add/ add 2 5 7 刪除主機 /hosts/del/(\d+)/ del 2 5 8 修改主機 /hosts/edit/(\d+)/ edit 2 5 以后獲取當前用戶權限后,數據結構化處理,並放入session { 1: { urls: [/users/,/users/add/ ,/users/del/(\d+)/], codes: [list,add,del] }, 2: { urls: [/hosts/,/hosts/add/ ,/hosts/del/(\d+)/], codes: [list,add,del] } } """ title = models.CharField(verbose_name='權限名稱',max_length=32) url = models.CharField(verbose_name='含正則的URL',max_length=255) code = models.CharField(verbose_name="權限代碼",max_length=32) group = models.ForeignKey(verbose_name='所屬權限組',to="PermissionGroup") # is_menu = models.BooleanField(verbose_name='是否是菜單') group_menu = models.ForeignKey(verbose_name='組內菜單',to="Permission",null=True,blank=True,related_name='xxx') class Department(models.Model): ''' 部門 ''' title = models.CharField(max_length=32) def __str__(self): return self.title class Host(models.Model): ''' 主機相關信息 ''' hostname = models.CharField(verbose_name='主機名', max_length=32) ip = models.CharField(verbose_name='IP',max_length=32)# ip = models.GenericIPAddressField(protocol='both') port = models.IntegerField(verbose_name="端口") user = models.ManyToManyField(verbose_name="用戶名",to='UserInfo',default=1) dp = models.ManyToManyField(verbose_name="部門",to="Department") def __str__(self): return self.hostname

rbac組件,目的是創建公共app,用於多所有系統增加權限管理。 1. 將rbac組件添加到project中 2. 將rbac/migrations目錄文件刪除(除__init__.py 以外) 3. 錄入權限: 5個類,7張表 4. 配置文件: - 中間件 MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', ... 'rbac.middlewares.rbac.RbacMiddleware', ] - 新增配置文件 # #################### 權限相關配置 ############################# PERMISSION_DICT_SESSION_KEY = "user_permission_dict_key" PERMISSION_MENU_SESSION_KEY = "user_permission_menu_key" REX_FORMAT = "^%s$" VALID_LIST = [ '/login/', '^/admin/.*', ] 5. 自動生成菜單 在你自己寫的母版中,引入rbac的inclusion_tag,示例: {% load rbac %} 導入rbac文件 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" href="/static/rbac/rbac.css" /> 引入rbac生成的菜單樣式 {% block css %} {% endblock %} </head> <body> <div class="pg-header"> 頭部菜單 </div> <div class="pg-content"> <div class="menu"> {% menu request %} 生成動態菜單 </div> <div class="content"> {% block content %} {% endblock %} </div> </div> {% block js %} {% endblock %} </body> </html>