rbac权限管理

概述

RBAC : 基于角色的权限访问控制(Role-Based Access Control),通过角色绑定权限,然后给用户划分角色。在web应用中,可以将权限理解为url,一个权限对应一个url。
基于角色的访问控制方法(RBAC)的显著的两大特征是:

1.由于角色/权限之间的变化比角色/用户关系之间的变化相对要慢得多,减小了授权管理的复杂性,降低管理开销。

2.灵活地支持企业的安全策略,并对企业的变化有很大的伸缩性。

实现步骤

1 创建项目,包含两个应用app01rbac

2 setting中配置:

INSTALLED_APPS  DATABASES  STATICFILES_DIRS

3 设计表关系

基于上述分析,在设计表关系时,起码要有5张表:用户,角色,权限,权限组,菜单:

  • 用户可以绑定多个角色,从而实现灵活的权限组合 :用户和角色,多对多关系
  • 每个角色下,绑定多个权限,一个权限也可以属于多个角色:角色和权限,多对多关系
  • 多个权限附属在一个权限组下,一个权限组下可以有多个权限:权限和权限组,多对一关系
  • 一个菜单包含多个权限组:权限组和菜单,多对一关系
  • 一个菜单下可能有多个子菜单,也可能有一个父菜单:菜单和菜单是自引用关系

在rbac的models中定义这几张表:

from django.db import models

# Create your models here.
class User(models.Model):
    """
    用户表
    """
    username = models.CharField(max_length=32,verbose_name='用户名')
    password = models.CharField(max_length=32,verbose_name='密码')
    roles = models.ManyToManyField('Role',verbose_name='与角色多对多绑定')

class Role(models.Model):
    """
    角色表,多对多绑定权限
    """
    name = models.CharField(max_length=32,verbose_name='角色名称')
    Permissions = models.ManyToManyField('Permission',verbose_name='与权限多对多绑定')


class Permission(models.Model):
    """
    权限表
    """
    name = models.CharField(max_length=32,verbose_name='权限名称')
    url = models.CharField(max_length=32,verbose_name='对应路径')
    code = models.CharField(max_length=32,verbose_name='别名')
    menu_group = models.ForeignKey(to='Permission',related_name='xxx',null=True,blank=True,default=None,verbose_name='所属菜单组')
    PermissionGroup = models.ForeignKey('PermissionGroup',null=True,verbose_name='所属权限组')


class PermissionGroup(models.Model):
    """
    权限分组
    """
    name = models.CharField(max_length=32,verbose_name='权限组名称')
    Menu = models.ForeignKey('Menu',null=True,verbose_name='所属菜单')

class Menu(models.Model):
    """
    菜单
    """
    name = models.CharField(max_length=32,verbose_name='菜单名称')

4 录入数据

models.Role.objects.create(name='CEO')
models.Role.objects.create(name='总监')
models.Role.objects.create(name='经理')
models.Role.objects.create(name='业务员')

models.User.objects.create(username='番禺',password=123)
models.User.objects.create(username='鲁宁',password=123)
models.User.objects.create(username='肾松',password=123)
models.User.objects.create(username='文飞',password=123)
models.User.objects.create(username='成栋',password=123)

models.Menu.objects.create(name='菜单一')
models.Menu.objects.create(name='菜单二')

models.PermissionGroup.objects.create(name='用户组',menu_id=1)
models.PermissionGroup.objects.create(name='订单组',menu_id=2)

models.Permission.objects.create(url='/userinfo/',name='用户列表',permissionGroup_id=1,code='list')
models.Permission.objects.create(url='/userinfo/add/',name='添加用户',permissionGroup_id=1,code='add')
models.Permission.objects.create(url='/userinfo/edit/(\d+)/',name='编辑用户',permissionGroup_id=1,code='edit')
models.Permission.objects.create(url='/userinfo/del/(\d+)/',name='删除用户',permissionGroup_id=1,code='del')
models.Permission.objects.create(url='/order/', name='订单列表',permissionGroup_id=2,code='list')
models.Permission.objects.create(url='/order/add/', name='添加订单',permissionGroup_id=2,code='add')
models.Permission.objects.create(url='/order/edit/(\d+)/', name='编辑订单',permissionGroup_id=2,code='edit')
models.Permission.objects.create(url='/order/del/(\d+)/', name='删除订单',permissionGroup_id=2,code='del')

models.Role.objects.get(name='CEO').permissions.add(1,2,3,4,5,6,7,8)
models.Role.objects.get(name='总监').permissions.add(1,2,5,6)
models.Role.objects.get(name='经理').permissions.add(1,5)
models.Role.objects.get(name='业务员').permissions.add(5)

models.User.objects.get(username='番禺').roles.add(1)
models.User.objects.get(username='鲁宁').roles.add(2)
models.User.objects.get(username='肾松').roles.add(3,4)
models.User.objects.get(username='文飞').roles.add(4)
models.User.objects.get(username='成栋').roles.add(4)

5 views.py 登录

from rbac.service.init_permission import init_permission

def login(request):
    if request.method == 'GET':
        return render(request,'login.html')
    else:
        username = request.POST.get('username')
        password = request.POST.get('password')
        user = models.User.objects.filter(username=username,password=password).first()
        if user:
            init_permission(request,user)
            return redirect('/index/')
        return render(request, 'login.html',{'msg':'用户名或密码错误'})

6 提取用户权限信息,写入session

在rbac应用下新建一个文件夹service,写一个脚本init_permission.py用来执行初始化权限的操作:用户登录后,取出其权限及所属菜单信息,写入session中

from collections import defaultdict
from django.conf import settings

def init_permission(request,user):
    #查询登录用户的权限等信息
    current_url = request.path_info
    userdata = user.roles.values('permissions__id'  # 权限ID
                                 , 'permissions__name'  # 权限名称
                                 , 'permissions__code'  # 别名
                                 , 'permissions__url'  # 权限路径
                                 , 'permissions__menu_group_id'  # 组内菜单ID,Null表示是菜单
                                 , 'permissions__permissionGroup_id'  # 权限所属组ID
                                 , 'permissions__permissionGroup__menu_id'# 菜单ID
                                 , 'permissions__permissionGroup__menu__name'  # 菜单名称
                                 ).distinct()
    #权限相关
    """
    permission_url_dict数据结构如下
    {
        1: {
            'codes': ['list', 'add', 'edit', 'del'], 
            'urls': ['/userinfo/', '/userinfo/add/', '/userinfo/edit/(\\d+)/', '/userinfo/del/(\\d+)/']
        },
        2: {
            'codes': ['list', 'add', 'edit', 'del'], 
            'urls': ['/order/', '/order/add/', '/order/edit/(\\d+)/', '/order/del/(\\d+)/']
        }
    }
    """
    permission_url_dict = defaultdict(lambda :{'codes':[],'urls':[]})
    for item in userdata:
        permission_url_dict[item['permissions__permissionGroup_id']]['codes'].append(item['permissions__code'])
        permission_url_dict[item['permissions__permissionGroup_id']]['urls'].append(item['permissions__url'])

    request.session[settings.PERMISSION_URL_KEY] = permission_url_dict#用户的权限信息保存到session中

    #菜单相关
    """
    permission_menu_list 数据结构如下
    [
        {'id': 1, 'title': '用户列表', 'url': '/userinfo/', 'menu_gp_id': None, 'menu_id': 1, 'menu_title': '菜单一'}, 
        {'id': 2, 'title': '添加用户', 'url': '/userinfo/add/', 'menu_gp_id': 1, 'menu_id': 1, 'menu_title': '菜单一'}, 
        {'id': 3, 'title': '编辑用户', 'url': '/userinfo/edit/(\\d+)/', 'menu_gp_id': 1, 'menu_id': 1, 'menu_title': '菜单一'}, 
        {'id': 4, 'title': '删除用户', 'url': '/userinfo/del/(\\d+)/', 'menu_gp_id': 1, 'menu_id': 1, 'menu_title':'菜单一'}, 
        {'id': 5, 'title': '订单列表', 'url': '/order/', 'menu_gp_id': None, 'menu_id': 2, 'menu_title': '菜单二'}, 
        {'id': 6, 'title': '添加订单', 'url': '/order/add/', 'menu_gp_id': 5, 'menu_id': 2, 'menu_title': '菜单二'},
        {'id': 7, 'title': '编辑订单', 'url': '/order/edit/(\\d+)/', 'menu_gp_id': 5, 'menu_id': 2, 'menu_title': '菜单二'}, 
        {'id': 8, 'title': '删除订单', 'url': '/order/del/(\\d+)/', 'menu_gp_id': 5, 'menu_id': 2, 'menu_title': '菜单二'}
    ]
    """
    #初步处理数据,在自定义标签中生成左侧菜单数据
    permission_menu_list = []
    for item in userdata:
        tpl = {
            'id': item['permissions__id'],
            'title': item['permissions__name'],
            'url': item['permissions__url'],
            'menu_gp_id': item['permissions__menu_group_id'],
            'menu_id': item['permissions__permissionGroup__menu_id'],
            'menu_title': item['permissions__permissionGroup__menu__name'],
        }
        permission_menu_list.append(tpl)
    request.session[settings.PERMISSION_MENU_KEY] = permission_menu_list #用户的菜单信息保存到session中

7 登陆成功后跳转到index页面,运用模板继承

8 定义中间件,处理所有请求。

在rbac应用下新建一个目录middleware,用来存放自定义中间件,新建rbac.py,在其中实现检查用户权限,控制访问:

import re
from django.conf import settings
from django.shortcuts import HttpResponse,render,redirect

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):
        current_url = request.path_info
        # 白名单处理
        for url in settings.VALID_URLS:
            if re.match(url, current_url):
                return None

        # 当前用户的权限列表
        permission_dict = request.session.get(settings.PERMISSION_URL_KEY)
        if not permission_dict:
            return redirect('/login/')

        flag = False
        for group_id,codes_urls in permission_dict.items():
            for permission_url in codes_urls['urls']:
                regex = "^{0}$".format(permission_url)
                if re.match(regex,current_url):
                    request.permission_code_list = codes_urls['codes']
                    flag = True
                    break
            if flag:
                break
        
        if not flag:
            return HttpResponse('无权访问')
MIDDLEWARE = ['rbac.middlewares.rbac.RbacMiddleware',]

9 自定义标签 - 左侧菜单
显示菜单要处理三个问题:

  • 第一,只显示用户权限对应的菜单,因此不同用户看到的菜单可能是不一样的
  • 第二,对用户当前访问的菜单下的url作展开显示,其余菜单折叠;
  • 第三,菜单的层级是不确定的(而且,后面要实现权限的后台管理,允许管理员添加菜单和权限);
    在rabc应用的目录下新建templatetags目录,写一个脚本my_tags.py,写一个函数menu_html,并加上自定义标签的装饰器:
import re
from django.template import Library
from django.conf import settings

register = Library()

@register.inclusion_tag('siderbar_menu.html')
def menu_html(request):
    menu_list = request.session.get(settings.PERMISSION_MENU_KEY)
    current_url = request.path_info
    """
    menu_list 数据结构
    [
        {'id': 1, 'title': '用户列表', 'url': '/userinfo/', 'menu_gp_id': None, 'menu_id': 1, 'menu_title': '菜单一', 'active': True}, 
        {'id': 2, 'title': '添加用户', 'url': '/userinfo/add/', 'menu_gp_id': 1, 'menu_id': 1, 'menu_title': '菜单一'}, 
        {'id': 3, 'title': '编辑用户', 'url': '/userinfo/edit/(\\d+)/', 'menu_gp_id': 1, 'menu_id': 1, 'menu_title': '菜单一'}, 
        {'id': 4, 'title': '删除用户', 'url': '/userinfo/del/(\\d+)/', 'menu_gp_id': 1, 'menu_id': 1, 'menu_title': '菜单一'}, 
        {'id': 5, 'title': '订单列表', 'url': '/order/', 'menu_gp_id': None, 'menu_id': 2, 'menu_title': '菜单二'}, 
        {'id': 6, 'title': '添加订单', 'url': '/order/add/', 'menu_gp_id': 5, 'menu_id': 2, 'menu_title': '菜单二'}, 
        {'id': 7, 'title': '编辑订单', 'url': '/order/edit/(\\d+)/', 'menu_gp_id': 5, 'menu_id': 2, 'menu_title': '菜单二'}, 
        {'id': 8, 'title': '删除订单', 'url': '/order/del/(\\d+)/', 'menu_gp_id': 5, 'menu_id': 2,'menu_title': '菜单二'}
    ]
    
    """

    """
    menu_dict 数据结构
    {
        1: {'id': 1, 'title': '用户列表', 'url': '/userinfo/', 'menu_gp_id': None, 'menu_id': 1, 'menu_title': '菜单一', 'active': True}, 
        5: {'id': 5, 'title': '订单列表', 'url': '/order/', 'menu_gp_id': None, 'menu_id': 2, 'menu_title': '菜单二'}
    }
    """

    menu_dict = {}
    for item in menu_list:
        if not item['menu_gp_id']:
            menu_dict[item['id']] = item

    for item in menu_list:
        regex = "^{0}$".format(item['url'])
        if re.match(regex,current_url):
            if not item['menu_gp_id']:
                menu_dict[item['id']]['active'] = True
            else:
                menu_dict[item['menu_gp_id']]['active'] = True

    """
    result数据结构:
    {
        1: {
            'menu_id': 1, 
            'menu_title': '菜单一', 
            'active': True, 
            'children': [
                {'title': '用户列表', 'url': '/userinfo/', 'active': True}
            ]
        },
         2: {
            'menu_id': 2, 
            'menu_title': '菜单二', 
            'active': None, 
            'children': [
                {'title': '订单列表','url': '/order/', 'active': None}
            ]
        }
    }

    """

    result = {}
    for item in menu_dict.values():
        active = item.get('active')
        menu_id = item['menu_id']
        if item['menu_id'] not in result:
            result[menu_id] = {
                'menu_id': menu_id,
                'menu_title': item['menu_title'],
                'active': active,
                'children': [
                    {'title': item['title'], 'url': item['url'], 'active': active},
                ]
            }
        else:
            result[menu_id]['children'].append({'title': item['title'], 'url': item['url'], 'active': active})
            if active:
                result[menu_id]['active'] = True

    return {'menu_dict': result}

需要渲染的标签页面:siderbar_menu.html

{% for k,item in menu_dict.items %}
    <div class="item">
        <div class="item-title">{{ item.menu_title }}</div>
        {% if item.active %}
            <div class="item-permission">
        {% else %}
            <div class="item-permission hide">
        {% endif %}
            {% for v in item.children %}

                {% if v.active %}
                    <a href="{{ v.url }}" class="active">{{ v.title }}</a>
                {% else %}
                    <a href="{{ v.url }}">{{ v.title }}</a>
                {% endif %}

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

10 页面的按钮按权限显示 -- 面向对象

class BasePagePermission(object):
    def __init__(self,code_list):
        self.code_list = code_list

    def has_add(self):
        if 'add' in self.code_list:
            return True

    def has_edit(self):
        if 'edit' in self.code_list:
            return True

    def has_del(self):
        if 'del' in self.code_list:
            return True
def userinfo(request):
    print(request.permission_code_list) #['list', 'add', 'edit', 'del']
    pagePermission = BasePagePermission(request.permission_code_list)
    user_list = models.User.objects.all()
    return render(request,'userinfo.html',{'user_list':user_list,'pagePermission':pagePermission})
{% extends 'base.html' %}

{% block content %}
    {% if pagePermission.has_add %}
        <a href="/userinfo/add">添加用户</a>
    {% endif %}
    <table>
        {% for user in user_list %}
            <tr>
                <td>{{ user.id }}</td>
                <td>{{ user.username }}</td>
                {% if pagePermission.has_edit %}
                <td><a href="">编辑</a></td>
                {% endif %}
                {% if pagePermission.has_del %}
                <td><a href="">编辑</a></td>
                {% endif %}
            </tr>
        {% endfor %}
    </table>
{% endblock %}

总结

RBAC中的代码
- models.py
- admin.py # 录入数据
- service.init_permission.py
- middlewares.rbac.py
- templatetags.my_tags.py
- static
配置文件

# 权限相关
PERMISSION_URL_KEY = 'aaaa'
PERMISSION_MENU_KEY = 'bbbbb'
VALID_URLS = [
    '/login/',
    '/logout/',
    '/index/',
    '/test/',
    'admin.*',
    'rbac.*',
]

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容