返回顶部

Django之Rbac组件开发小记

一. 简介

  RBAC  是基于角色的访问控制(Role-Based Access Control )在 RBAC  中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。

 

二. 表结构设计

from django.db import models

# Create your models here.

__all__ = ['Menu', 'Permission', 'Role', 'UserInfo']

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)
    name = models.CharField(verbose_name='别名', max_length=32)
    menu = models.ForeignKey('Menu', verbose_name='所属菜单', null=True, blank=True,help_text='null表示不是二级菜单,'
                                                                                          '非null表示是二级菜单',on_delete=models.CASCADE)

    pid = models.ForeignKey('self', verbose_name='关联的权限', null=True, blank=True, on_delete=models.CASCADE)

    def __str__(self):
        return self.title

class Role(models.Model):
    title = models.CharField(max_length=32, verbose_name='角色名称')
    permissions = models.ManyToManyField('Permission', blank=True, verbose_name='拥有的权限')

    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('Role', blank=True, verbose_name='拥有的角色')

    def __str__(self):
        return self.name

permission表一部分截图:

 

 注意:

1.写路由的时候,如果用path的话,有名分组要用pk为参数,否则,自动注入路由时,无法帮你转换成正则。

path('customer/edit/<int:pk>/', customer.customer_edit, name='customer_edit'),

2. 在models中定义一个__all__列表,可以在admin中注册时,通过反射快速注册。

for table in models.__all__:
    admin.site.register(getattr(models, table))

 

三. 权限初始化和中间件校验

权限初始化

我们在写完业务逻辑中的登录后,可以通过init_permission方法进行权限的初始化,比如:

 

 init_permission中主要做的事情:

1.获取当前用户的所有权限(用户---> 角色 ----> 权限)

2.通过权限的queryset, 生成permission_dict和menu_dict 两个字典(除了权限校验功能,还是为了权限粒度到按钮级别,生成导航条)

3.把这两个字典写入session。(这里直接通过数据库的sessio表简单存)

from django.conf import settings


def init_permission(current_user, request):

    permission_queryset = current_user.roles.filter(permissions__isnull=False).values('permissions__id',
                                                                                      'permissions__title',
                                                                                      'permissions__url',
                                                                                      'permissions__name',
                                                                                      'permissions__pid_id',
                                                                                      'permissions__pid__title',
                                                                                      'permissions__pid__url',
                                                                                      'permissions__menu_id',
                                                                                      'permissions__menu__title',
                                                                                      'permissions__menu__icon'
                                                                                      ).distinct()

    permission_dict = {}
    menu_dict = {}
    for item in permission_queryset:
        permission_dict[item['permissions__name']] = {
            'id': item['permissions__id'],
            'title': item['permissions__title'],
            'url': item['permissions__url'],
            'pid': item['permissions__pid_id'],
            'pid_title': item['permissions__pid__title'],
            'pid_url': item['permissions__pid__url']
        }

        menu_id = item['permissions__menu_id']
        if not menu_id:
            continue
        node = {'id': item['permissions__id'], 'title': item['permissions__title'], 'url': item['permissions__url']}
        if menu_id in menu_dict:
            menu_dict[menu_id]['children'].append(node)
        else:
            menu_dict[menu_id] = {
                'title': item['permissions__menu__title'],
                'icon': item['permissions__menu__icon'],
                'children': [node, ]
            }
    # 权限和菜单信息放入session
    request.session[settings.PERMISSION_SESSION_KEY] = permission_dict
    request.session[settings.MENU_SESSION_KEY] = menu_dict

登录校验

通过中间件的process_request,对每一个请求到来之前做权限校验。

import re
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
from django.conf import settings



class RbacMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        current_path = request.path_info
        # print(current_path)
        for item in settings.VALID_URL_LIST:
            if re.match(item, current_path):
                return None

        permission_dict = request.session.get(settings.PERMISSION_SESSION_KEY)
        if not permission_dict:
            return HttpResponse('未获取到权限信息,请先登录!')

        flag = False
        url_record = [
            {'title': '首页', 'url': '#'}
        ]

        for item in settings.NO_PERMISSION_LIST: # 需要登陆,但是无需进行权限校验
            if re.match(item, current_path):
                request.current_selected_permission = 0
                request.breadcrumb = url_record
                return None

        for item in permission_dict.values():
            reg = '^%s$' % item['url']
            if re.match(reg, current_path):
                flag = True
                request.current_selected_permission = item['pid'] or item['id']
                if not item['pid']:
                    # 是一个二级菜单
                    url_record.extend([{'title': item['title'], 'url': item['url'], 'class': 'active'}])
                else:
                    url_record.extend([{'title': item['pid_title'], 'url': item['pid_url']}])
                    url_record.extend([{'title': item['title'], 'url': item['url'], 'class': 'active'}])
                request.breadcrumb = url_record
                break


        if not flag:
            return HttpResponse('无权访问')

注意:

# 白名单表示不需要登录,直接就可以访问
VALID_URL_LIST = [ '/web/login/', '/admin/.*' ]
# 需要登录,但是可以是不需要任何权限的页面 NO_PERMISSION_LIST
= [ '/web/index/', '/web/logout/', ]

 

 

四. 二级菜单和路劲导航

先看一下效果:

 

 具体代码

新建一个templatetags文件夹,通过inclusion_tag来实现:

from collections import OrderedDict
from django import template
from django.conf import settings

from utils import reverse_url

register = template.Library()


@register.inclusion_tag('rbac/multi_menu.html')
def multi_menu(request):

    menu_dict = request.session.get(settings.MENU_SESSION_KEY)
    key_list = sorted(menu_dict)
    ordered_dict = OrderedDict()
    for key in key_list:
        val = menu_dict[key]
        val['class'] = 'hide'

        for per in val['children']:
            if per['id'] == request.current_selected_permission:
                per['class'] = 'active'
                val['class'] = ''
        ordered_dict[key] = val

    return {'menu_dict': ordered_dict}


@register.inclusion_tag('rbac/breadcrumb.html')
def breadcrumb(request):
    return {'record_list':request.breadcrumb}

multi_menu.html:

<div class="multi-menu">
    {% for item in menu_dict.values %}
        <div class="item">
            <div class="title"><span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}</div>
            <div class="body {{ item.class }}">
                {% for per in item.children %}
                    <a href="{{ per.url }}" class="{{ per.class }}">{{ per.title }}</a>
                {% endfor %}

            </div>

        </div>
    {% endfor %}
</div>

breadcrumb.html:

<div>
    <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">
        {% for item in record_list %}
            {% if item.class %}
                <li class="{{ item.class }}">{{ item.title }}</li>
            {% else %}
                <li><a href="{{ item.url }}">{{ item.title }}</a></li>
            {% endif %}
        {% endfor %}
    </ol>
</div>

粒度控制到按钮

@register.filter
def has_permission(request, name):
    if name in request.session.get(settings.PERMISSION_SESSION_KEY):
        return True

页面中就可以这样使用:

注意:使用之前除了导入你的tags文件,还要在settings中注册:

{% if request|has_permission:'customer_add' %}
                <a class="btn btn-default" href="{% url 'web:customer_add' %}">
                    <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户
                </a>
                {% endif %}

 

五 .角色管理,用户管理

主要用到modelform知识点

注意:

# 第一: 对自定义的新增字段添加属性只能通过重写init方法来实现
class UserAddModelForm(BaseModelForm):
confirm_password = forms.CharField(label='确认密码', max_length=64)

class Meta:
model = models.UserInfo
fields = ['name','password','confirm_password', 'email']
widgets = {
'password': forms.PasswordInput(),
}

def __init__(self, *args, **kwargs):
super(UserAddModelForm, self).__init__(*args, **kwargs)
self.fields['confirm_password'].widget = forms.PasswordInput()
self.fields['confirm_password'].widget.attrs['class'] = 'form-control'
 

 

六. 一二三级菜单管理

也是主要是通过modelform来操作增删改查。

注意:

# 第一点:给Charfield字段定制可选项图标: 插件用的是RadioSelect
ICON_LIST = [
    ['fa-hand-scissors-o', '<i aria-hidden="true" class="fa fa-hand-scissors-o"></i>'],
    ['fa-hand-spock-o', '<i aria-hidden="true" class="fa fa-hand-spock-o"></i>'],
]
for item in ICON_LIST:
    item[1] =mark_safe(item[1])


class MenuModelForm(forms.ModelForm):
    class Meta:
        model = models.Menu
        fields = ['title', 'icon']
        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control'}),
            'icon': forms.RadioSelect(
                choices=ICON_LIST,
                attrs={'class': 'clearfix'}
            )
        }

# 第二点: 对于二级菜单,新增的时候,默认归属于自己的一级菜单: 通过initial参数传一个字典来指定
def secondmenu_add(request, menu_id):
menu_object = models.Menu.objects.filter(pk=menu_id).first()
if not menu_object:
return HttpResponse('一级菜单不存在!')
if request.method == 'GET':
form = SecondMenuModelForm(initial={'menu': menu_object})
return render(request, 'rbac/change.html', {'form': form})
form = SecondMenuModelForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse_url(request, 'rbac:menu_list'))
return render(request, 'rbac/change.html', {'form': form})

# 第三点:通过form.instance.字段在save之前更新值
def permissions_add(request, secondmenu_id):

second_menu_obj = models.Permission.objects.filter(pk=secondmenu_id).first()
if not second_menu_obj:
return HttpResponse('二级菜单不存在!')
if request.method == 'GET':
form = PermissionModelForm()
return render(request, 'rbac/change.html', {'form': form})
form = PermissionModelForm(data=request.POST)
if form.is_valid():
form.instance.pid = second_menu_obj
form.save()
return redirect(reverse_url(request, 'rbac:menu_list'))
return render(request, 'rbac/change.html', {'form': form})

 

七. 批量操作权限

主要用到是Django的formset,对一批form进行操作。

步骤:

1.创建form类,定义字段。

class MultiAddPermissionForm(forms.Form):
    title = forms.CharField(
        widget=forms.TextInput(attrs={'class': 'form-control'})
    )
    url = forms.CharField(
        widget=forms.TextInput(attrs={'class': 'form-control'})
    )

    name = forms.CharField(
        widget=forms.TextInput(attrs={'class': 'form-control'})
    )
    menu_id = forms.ChoiceField(
        choices=[(None, '----')],
        widget=forms.Select(attrs={'class': 'form-control'}),
        required=False
    )
    pid_id = forms.ChoiceField(
        choices=[(None, '----')],
        widget=forms.Select(attrs={'class': 'form-control'}),
        required=False
    )

    def __init__(self, *args, **kwargs):
        super(MultiAddPermissionForm, self).__init__(*args, **kwargs)
        self.fields['menu_id'].choices += models.Menu.objects.values_list('id', 'title')
        self.fields['pid_id'].choices += models.Permission.objects.filter(pid_id__isnull=True).\
            exclude(menu_id__isnull=True).values_list('id','title')

2. 创建formset_factory对象:

from django.forms import formset_factory
generate_formset_class = formset_factory(MultiAddPermissionForm, extra=0)

3. 把包含form中定义字段的对象,通过initial参数传入generate_formset_class中:

    if not generate_formset:
        generate_name_list = router_name_set - permissions_name_set
        generate_formset = generate_formset_class(
            initial=[row_dict for name, row_dict in all_url_dict.items() if name in generate_name_list]
        )

4. 在页面中使用:

<form action="?type=generate" method="post">
            {% csrf_token %}
            {{ generate_formset.management_form }}  # 别忘记了加上这个
            <div class="panel panel-default">
                <!-- Default panel contents -->
                <div class="panel-heading">
                    <i class="fa fa-th-list" aria-hidden="true"></i> 待新建权限列表
                    <button href="#" class="right btn btn-primary btn-xs"
                            style="padding: 2px 8px;margin: -3px;">
                        <i class="fa fa-save" aria-hidden="true"></i>
                        保存
                    </button>
                </div>
                <!-- Table -->
                <table class="table">
                    <thead>
                    <tr>
                        <th>序号</th>
                        <th>名称</th>
                        <th>URL</th>
                        <th>别名</th>
                        <th>菜单</th>
                        <th>父权限</th>
                    </tr>
                    </thead>
                    <tbody>
                    {% for form in generate_formset %}
                        <tr>
                            <td>{{ forloop.counter }}</td>
                            {% for field in  form %}
                                <td>{{ field }}<span style="color: red;">{{ field.errors.0 }}</span></td>
                            {% endfor %}
                        </tr>
                    {% endfor %}

                    </tbody>
                </table>
            </div>
        </form>

5. 后台,添加和更新代码:

    if request.method == 'POST' and post_type == 'generate':
        formset = generate_formset_class(data=request.POST)
        if formset.is_valid():
            object_list = []
            post_row_dict = formset.cleaned_data
            has_error = False
            for i in range(0, formset.total_form_count()):
                row_dict = post_row_dict[i]
                try:
                    new_object = models.Permission(**row_dict)
                    new_object.validate_unique()  # 检查唯一性约束是否满足
                    object_list.append(new_object)
                except Exception as e:
                    formset.errors[i].update(e)
                    generate_formset = formset
                    has_error = True
            if not has_error:
                models.Permission.objects.bulk_create(object_list, batch_size=100)
        else:
            generate_formset = formset

    if request.method == 'POST' and post_type == 'update':
        formset = update_formset_class(data=request.POST)
        if formset.is_valid():
            post_row_dict = formset.cleaned_data
            for i in range(0, formset.total_form_count()):
                row_dict = post_row_dict[i]
                permission_id = row_dict.pop('id')
                try:
                    row_object = models.Permission.objects.filter(pk=permission_id).first()
                    for k,v in row_dict.items():
                        setattr(row_object, k, v)  # 通过反射进行对象的值更新
                    row_object.validate_unique()
                    row_object.save()
                except Exception as e:
                    formset.errors[i].update(e)
                    update_formset = formset
        else:
            update_formset =formset

注意:

# 第一点: 从数据库中,对choiceField字段中的choices增加选项:
class MultiEditPermissionForm(forms.Form):
id = forms.IntegerField(
widget=forms.HiddenInput()
)

title = forms.CharField(
widget=forms.TextInput(attrs={'class': 'form-control'})
)
url = forms.CharField(
widget=forms.TextInput(attrs={'class': 'form-control'})
)

name = forms.CharField(
widget=forms.TextInput(attrs={'class': 'form-control'})
)

menu_id = forms.ChoiceField(
choices=[(None, '-----')],
widget=forms.Select(attrs={'class': 'form-control'}),
required=False
)
pid_id = forms.ChoiceField(
choices=[(None, '-----')],
widget=forms.Select(attrs={'class': 'form-control'}),
required=False,
)

def __init__(self, *args, **kwargs):
super(MultiEditPermissionForm, self).__init__(*args, **kwargs)
self.fields['menu_id'].choices += models.Menu.objects.values_list('id', 'title')
self.fields['pid_id'].choices += models.Permission.objects.filter(pid__isnull=True).exclude(
menu__isnull=True
).values_list('id', 'title')
注意:
id = forms.IntegerField(
widget=forms.HiddenInput()
)
这是一个隐藏的input框,作用是通过formset来批量操作form的时候,可以pop掉这个id,来filter找到这个对象。

permission_id = row_dict.pop('id')
row_object = models.Permission.objects.filter(pk=permission_id).first()

# 第二点: 同一个页面有多个form表单,都需要通过post提交数据,后台怎么区分是哪一个form的数据?
方法一:在action中通过参数的形式进行区分
<form action="?type=generate" method="post">

方法二:在form中定义一个隐藏的input标签,通过name属性和value属性进行区分。
<input type="hidden" name="type" value="permission">
 

 

八. 分配权限

构造一个数据结构:

menu_list = [
    {'id': 1, 'title': '菜单1'},
    {'id': 2, 'title': '菜单2'},
    {'id': 3, 'title': '菜单3'},
]

menu_dict = {}
"""
{
    1:{'id': 1, 'title': '菜单1'},
    2:{'id': 2, 'title': '菜单2'},
    3:{'id': 3, 'title': '菜单3'},
}
"""
for item in menu_list:
    menu_dict[item['id']] = item

# menu_dict[2]['title'] = '666'

menu_dict[2]['children'] = [11,22]

print(menu_list)  # 就会多一个'children': [11,22]的键值对

 

posted @ 2020-10-12 00:23  muguangrui  阅读(373)  评论(0编辑  收藏  举报