前言
RBAC即Role-Based Access Control基于角色的访问控制。
通俗说,就是,权限不会直接分配到用户,而是分配到用户所拥有得角色
这样的好处是什么?
好处是当用户特别多时,管理员分配权限能累死人,基于RBAC更适合企业应用得权限控制。
RBAC的4大模型
RBAC根据这套模型功能的复杂程度不同,由简单到复杂又可以分为RBAC-0、RBAC-1、RBAC-2、RBAC-3四个层级。
1.RBAC-0
RBAC-0是最基础的,就是在用户与角色、角色与权限间建立关系,每种关系均为多对多。
2.RBAC-1
RBAC-1是在RBAC-0的基础上,增加了继承关系。就是一个角色只能有另一角色的部分权限,这个角色的权限大小受另一角色权限影响。
简单说,角色2由角色1派生,那么角色2所有得权限必然小于角色1,角色1拥有权限1,2,而角色2只有拥有权限。
角色分级
3.RBAC-2
RBAC-2模型,其实就是通过限制滥用角色,防止角色冲突。
一个简单例子,你不能即是老板也是老板秘书,这样关系就乱了
这里就是用户3不能同时拥有角色2,3。
4.RBAC-3
RBAC-3是RBAC-1与RBAC-2的结合,就是既有继承关系,又有限制条件,基础都一样。
5.权限管理
关于RBAC模型细节说明
1.数据权限
数据权限是指用户是否能够看到某些数据。主要应用在数据有保密要求,或数据量大,需按用户或角色来进行区分时。例如:财务主管在后台可以看到公司所有人的工资流水,但普通员工只能看到自己的工资流水。
在这里,有的同学或许认为,既然我们的权限是跟角色关联,为什么“查看工资流水”这个权限精确到每个用户了呢?
其实这就是RBAC-2的一种,权限与角色关联后增加了限制条件,不过这种限制条件无法在页面上灵活配置。
数据权限的颗粒度由粗到细可以分为菜单级、栏目级、字段级,一般配置页面都可以灵活操作。
2.操作权限
操作权限是指用户是否能够操作对应按钮。需要先有数据权限,才有操作权限,所以需要增加系统自动校验:
选择了操作权限,就默认勾选了查看(数据)权限;
取消了数据权限,就自动取消了操作权限
以上就是我们做单系统的权限设计分享的内容。在多系统的权限设计中,虽然理论基础一样,但因为涉及到多个系统,所以产生了很多其他问题,需要另外解决。
3.多级数据权限得控制
有这样一种需求,每个小区管理员之间,只能看到自己小区得数据,很显然他们是同级别得,功能权限相同,但是数据权限不同
对于这种得需求,可以通过硬编码得方式去解决,但是还有一种更好的解决方案
基于 用户——用户组——角色——权限 这样的一种方式来解决;
部门与角色关联, 类似实际生产生活中的技术部经理和产品部经理的感觉,
这样每个小区可以设置为不同的部门,都是分配一个小区管理员,这样就可以通过部门来划分一个大方向的数据权限了
Django实现RBAC0
功能分析:
一个成熟的web应用,对权限的控制、管理是不可少的;对于一个web应用来说是什么权限?
这要从web应用的使用说起,用户在浏览器输入一个url,访问server端,server端返回这个url下对应的资源;
所以 对于用户来说 1个可以访问url 就等于1个权限
比如某人开发了一个web应用包含以下5个url,分别对于不同资源;
1、91.91p15.space/Chinese/
2、91.91p15.space/Japanese and Korean/
3、91p15.space/Euramerican/
4、91p15.space/Latin America/
5、91p15.space/African/
--------------------------------------------------------------------------------------------------------
普通用户:可以访问 5
白金用户:可以访问 4、5、1
黄金用户:可以访问1、2、3、4、5
为什么某些网站会为广大用户做角色划分呢(比如 普通、会员、黑金、白金)?
因为给用户归类后,便于权限的划分、控制、管理;
所以我们把这种基于角色来做得权限控制,称为RBAC(Role Basic Access Control)
RBAC0模型数据库表结构设计
1.用户表
用户表和角色表为多对多关系,1个用户可以有多个角色,1个角色可以被多个用户划分;
2.角色表
角色表和权限也是多对多关系,一个角色可以有多个权限,一个权限可以划分给多个角色
3.菜单表
用于在前端引导用户找到自己的权限,并可以设置多级菜单对用户权限进行划分;所以权限表和菜单表是1对多关系;
由于需要构建多级菜单,并且拥有嵌套关系,所以菜单表自引用;
启发:一般设计包含层级结构嵌套,切嵌套的层级无法预测的表结构使用自关联;(表1外键-----》表2----》外键表3是行不通的,因为无法预测嵌套层级的深度)
例如:多级评论(无法预测,评论树的深度)
modal.py数据模型
1、创建一个独立的app作为公共模块,以备后期遇到权限相关项目时使用;
from django.db import models from django.db import models class Menu(models.Model): ''' 菜单表''' caption=models.CharField(max_length=32) parent=models.ForeignKey('Menu',null=True,blank=True) #自关联 def __str__(self): caption_list = [self.caption,] p=self.parent while p: #如果有父级菜单,一直向上寻找 caption_list.insert(0,p.caption) p=p.parent return "-".join(caption_list) class Permission(models.Model): '''权限表''' title = models.CharField(max_length=64) url = models.CharField(max_length=255) menu = models.ForeignKey('Menu', null=True, blank=True)#和菜单是1对多关系 def __str__(self): return '权限名称: %s--------权限所在菜单 %s'% (self.title,self.menu) class Role(models.Model): '''角色表''' rolename=models.CharField(max_length=32) permission=models.ManyToManyField('Permission') def __str__(self): return '角色: %s--------权限 %s'% (self.rolename,self.permission) class UserInfo(models.Model): '''用户表''' name=models.CharField(max_length=32) pwd=models.CharField(max_length=64) rule=models.ManyToManyField('Role') def __str__(self): return self.name
权限初始化设置、中间件获取、判断、生成权限菜单;
当用户登录之后获取到用户名、密码查询用户表连表查询得到角色、权限信息,写入当前用户session(用session来保存用户的权限信息);
写入session之后每次用户请求到来,通过Django中间件判断用户权限;
1.用户首次登录,初始时该用户权限,写入session;
from app02 import models from app02.service import init_session from django.conf import settings import re def login(reqeust): if reqeust.method == 'GET': return render(reqeust, 'login.html') else: user = reqeust.POST.get('user') pwd = reqeust.POST.get('pwd') user_obj = models.UserInfo.objects.filter(name=user, pwd=pwd).first() if user: # init_session(reqeust,user_obj) init_session.per(reqeust,user_obj)#用户首次登录初始化用户权限信息 return redirect('/index/') else: return render(reqeust, 'login.html') def index(request): return HttpResponse('INDEX') def test_query(request): return render(request,'test.html')
from django.conf import settings from .. import models def per(reqeust,user_obj): permission_list = user_obj.rule.values('permission__title', 'permission__url', 'permission__menu_id', ).distinct() permission_urllist = [] # 当前用户可以访问的url(权限列表) permission_menulist = [] # 当前用户应该挂靠到菜单上显示的权限 for iteam in permission_list: permission_urllist.append(iteam['permission__url']) if iteam['permission__menu_id']: temp = {'title': iteam['permission__title'], 'url': iteam['permission__url'], 'menu_id': iteam['permission__menu_id']} permission_menulist.append(temp) menulist = list(models.Menu.objects.values('id', 'caption', 'parent_id')) # 获取所有菜单(以便当前用户的菜单挂靠) from django.conf import settings reqeust.session[settings.SESSION_PERMISSION_URL_KEY] = permission_urllist reqeust.session[settings.SESSION_PERMISSION_MENU_URL_KEY] = { 'k1': permission_menulist, 'k2': menulist }
2.用户再次登录通过Django中间件 检查当前用户session中携带的权限信息,进而判断用户是否对当前request.path有访问权限?;
from django.utils.deprecation import MiddlewareMixin import re from django.shortcuts import render,redirect,HttpResponse from django.conf import settings class Mddile1(MiddlewareMixin): def process_request(self,request): #如果用户访问的url是登录、注册页面,记录到白名单,放行 for url in settings.PASS_URL_LIST: if re.match(url,request.path_info): return None Permission_url_list=request.session.get(settings.SESSION_PERMISSION_URL_KEY) #如果用户访问的url 不在当前用户权限之内 返回login页面 if not Permission_url_list: return redirect(settings.LOGIN_URL) current_url=request.path_info #由于数据库的数据,可能是正则所有 一定要精确匹配 flag=False for url in Permission_url_list: url='^%s$'%(url) if re.match(url,current_url): flag=True break if not flag: if settings.DEBUG: #如果是程序调试应该 显示用户可以访问的权限 url_html='<br/>'.join(Permission_url_list) return HttpResponse('无权访问您可以访问%s'%url_html) else: return HttpResponse('没有权限') def process_response(self, request,response): return response
根据用户权限生成菜单
当用户使用当前访问的通过中间件之后,要做的事情只有2步;
1、根据用户session中的权限列表,生成该用户的菜单;
2、根据用户访问的当前url,把这个菜单 从当前url(权限)从下到上展开;
def test_query(request): menu_permission_list=request.session[settings.SESSION_PERMISSION_MENU_URL_KEY] permission_list=menu_permission_list['k1'] #获取需要挂靠在菜单上显示的权限 menu_list=menu_permission_list['k2'] #获取全部菜单 all_menu_dict={} # status 是用户全部权限,挂靠显示的菜单; # open 当前url(权限)对应的父级菜单展开? for item in menu_list: item['child']=[] item['status']=False item['open']=False all_menu_dict[item['id']]=item current_url=request.path_info for row in permission_list: row['status'] = True row['open']=False if re.match('^%s$'% (row['url']),current_url): row['open']=True all_menu_dict[row['menu_id']]['child'].append(row) pid=row['menu_id'] while pid: all_menu_dict[pid]['status']=True pid=all_menu_dict[pid]['parent_id'] if row['open']: PID=row['menu_id'] while PID: all_menu_dict[PID]['open']=True PID=all_menu_dict[PID]['parent_id'] return HttpResponse('OK')
自定义模板语言 simple_tag 把用户菜单渲染到前端
from django.template import Library from django.conf import settings import re,os from django.utils.safestring import mark_safe register=Library() #生成菜单所有数据 def men_data(request): menu_permission_list = request.session[settings.SESSION_PERMISSION_MENU_URL_KEY] permission_list = menu_permission_list['k1'] # 获取需要挂靠在菜单上显示的权限 menu_list = menu_permission_list['k2'] # 获取全部菜单 all_menu_dict = {} # status 是用户全部权限,挂靠显示的菜单; # open 当前url(权限)对应的父级菜单展开? # 把用户所有的权限挂靠到对应的菜单 for item in menu_list: item['child'] = [] item['status'] = False item['open'] = False all_menu_dict[item['id']] = item current_url = request.path_info for row in permission_list: row['status'] = True row['open'] = False if re.match('^%s$' % (row['url']), current_url): row['open'] = True all_menu_dict[row['menu_id']]['child'].append(row) pid = row['menu_id'] while pid: all_menu_dict[pid]['status'] = True pid = all_menu_dict[pid]['parent_id'] if row['open']: PID = row['menu_id'] while PID: all_menu_dict[PID]['open'] = True PID = all_menu_dict[PID]['parent_id'] # 把用户所有菜单挂父级菜单 res = [] for k, v in all_menu_dict.items(): if not v.get('parent_id'): res.append(v) else: pid = v.get('parent_id') all_menu_dict[pid]['child'].append(v) return res #生成菜单所用HTML def process_menu_html(menu_list): #盛放菜单所用HTML标签 tpl1 = """ <div class='rbac-menu-item'> <div class='rbac-menu-header'>{0}</div> <div class='rbac-menu-body {2}'>{1}</div> </div> """ #盛放权限的HTML tpl2 = """ <a href='{0}' class='{1}'>{2}</a> """ html='' for item in menu_list: if not item['status']: continue else: if item.get('url') : # 权限 html+= tpl2.format(item['url'],'rbac_active' if item['open'] else '',item['title']) else: #菜单 html+= tpl1.format(item['caption'],process_menu_html(item['child']),''if item['open'] else 'rbac-hide') return mark_safe( html) @register.simple_tag def rbac_menus(request): res= men_data(request) html=process_menu_html(res) return html @register.simple_tag def rbac_css(): file_path = os.path.join('app02', 'theme', 'rbac.css') if os.path.exists(file_path): return mark_safe(open(file_path, 'r', encoding='utf-8').read()) else: raise Exception('rbac主题CSS文件不存在') @register.simple_tag def rbac_js(): file_path = os.path.join('app02', 'theme', 'rbac.js') if os.path.exists(file_path): return mark_safe(open(file_path, 'r', encoding='utf-8').read()) else: raise Exception('rbac主题JavaScript文件不存在')
使用 ModelForm组件 填充插件中数据
1、 Modal Form插件的简单使用
Modal Form 顾名思义 就是把Modal和Form验证的功能紧密集合起来,实现对数据库数据的增加、编辑操作;
添加
from app02 import models from django.forms import ModelForm class UserModalForm(ModelForm): class Meta: model=models.UserInfo #(该字段必须为 model 数据库中表) fields= '__all__' #(该字段必须为 fields 数据库中表) def add(request): # 实例化models_form if request.method=='GET': obj = UserModalForm() return render(request,'rbac/user_add.html',locals()) else: obj=UserModalForm(request.POST) if obj.is_valid(): data=obj.cleaned_data obj.save() #form验证通过直接 添加用户信息到数据库 return render(request, 'rbac/user_add.html', locals())
使用
def user_edit(request): pk = request.GET.get('id') user_obj = models.UserInfo.objects.filter(id=pk).first() if request.method=='GET': if not user_obj: return redirect('/app02/user_edit/') else: #在form表单中自动填充默认值 model_form_obj=UserModalForm(instance=user_obj) return render(request,'rbac/user_edit.html',locals()) else: #修改数据 需要instance=user_obj model_form_obj = UserModalForm(request.POST,instance=user_obj) if model_form_obj.is_valid(): model_form_obj.save() return redirect('/app02/userinfo/')
2、Modal Form 参数设置
from django.shortcuts import render,HttpResponse,redirect from app02 import models from django.forms import ModelForm from django.forms import widgets as wid from django.forms import fields as fid class UserModalForm(ModelForm): class Meta: model=models.UserInfo #(该字段必须为 model 数据库中表) fields= '__all__' #(该字段必须为 fields '__all__',显示数据库中所有字段, # fields=['指定字段'] # exclude=['排除指定字段'] ) # fields=['name',] # exclude=['pwd'] #error_messages 自定制错误信息 error_messages={'name':{'required':'用户名不能为空'}, 'pwd': {'required': '密码不能为空'}, } #widgets 自定制插件 # widgets={'name':wid.Textarea(attrs={'class':'c2'})} #由于数据库里的字段 和前端显示的会有差异,可以使用 labels 定制前端显示 labels={'name':'姓名','pwd':'密码','rule':'角色'} #自定制 input标签 输入信息提示 help_texts={'name':'别瞎写,瞎写打你哦!'} #自定制自己 form 字段.CharField() email()等 field_classes={ 'name':fid.CharField }
3、添加数据库之外的字段,实时数据更新
ModelForm 可以结合Model把所有数据库字段在页面上生成,也可以增加额外的字段;
规则:如果增加的字段和数据里的filed重名则覆盖,不重名则新增;
也可以通过重写__init__ ,每次实例化1个form对象,实时更新数据;
class PermissionModelForm(ModelForm): #ModelForm 可以结合Model把所有数据库字段在页面上生成,也可以增加额外的字段; url=fields.ChoiceField() class Meta: fields = "__all__" model = models.Permission #注意不是models def __init__(self,*args,**kwargs): #重写父类的 __init__方法,每次实例化实时更新 form中的数据 super(PermissionModelForm,self).__init__(*args,**kwargs) from pro_crm.urls import urlpatterns self.fields['url'].choices=get_all_url(urlpatterns,'/', True)
总结
如何把权限精确到按钮,按钮就是子菜单就是一个url
权限管理的思路是
把用户权限记录到数据库里面
当用户首次登录时,从数据库里取出数据把用户的权限(url)和挂靠的菜单菜单/写入到session中
以后每次访问在中间件进行check;
难度在于:多级菜单之间的拼接挂靠会用到递归,所以我选择了二级菜单;