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>