前言

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
View Code

权限初始化设置、中间件获取、判断、生成权限菜单;

当用户登录之后获取到用户名、密码查询用户表连表查询得到角色、权限信息,写入当前用户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
    }
init_session.per

 

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
View Code

根据用户权限生成菜单

当用户使用当前访问的通过中间件之后,要做的事情只有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')
View Code

 

自定义模板语言 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文件不存在')
View Code

使用 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())
View Code

 使用

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/')
View Code

 

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
        }
View Code

 

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)
View Code

 

总结

如何把权限精确到按钮,按钮就是子菜单就是一个url

权限管理的思路是 

把用户权限记录到数据库里面

当用户首次登录时,从数据库里取出数据把用户的权限(url)和挂靠的菜单菜单/写入到session中

以后每次访问在中间件进行check;

 

难度在于:多级菜单之间的拼接挂靠会用到递归,所以我选择了二级菜单;

 

参考

posted on 2017-09-22 15:42  Martin8866  阅读(12714)  评论(1编辑  收藏  举报