rbac權限管理

概述

RBAC : 基于角色的權限訪問控制(Role-Based Access Control)希停,通過角色綁定權限下隧,然后給用戶劃分角色咨演。在web應用中辰企,可以將權限理解為url风纠,一個權限對應一個url。
基于角色的訪問控制方法(RBAC)的顯著的兩大特征是:

1.由于角色/權限之間的變化比角色/用戶關系之間的變化相對要慢得多蟆豫,減小了授權管理的復雜性议忽,降低管理開銷。

2.靈活地支持企業(yè)的安全策略十减,并對企業(yè)的變化有很大的伸縮性。

實現(xiàn)步驟

1 創(chuàng)建項目愤估,包含兩個應用app01帮辟,rbac

2 setting中配置:

INSTALLED_APPS  DATABASES  STATICFILES_DIRS

3 設計表關系

基于上述分析,在設計表關系時玩焰,起碼要有5張表:用戶由驹,角色,權限,權限組蔓榄,菜單:

  • 用戶可以綁定多個角色并炮,從而實現(xiàn)靈活的權限組合 :用戶和角色,多對多關系
  • 每個角色下甥郑,綁定多個權限逃魄,一個權限也可以屬于多個角色:角色和權限,多對多關系
  • 多個權限附屬在一個權限組下澜搅,一個權限組下可以有多個權限:權限和權限組伍俘,多對一關系
  • 一個菜單包含多個權限組:權限組和菜單,多對一關系
  • 一個菜單下可能有多個子菜單勉躺,也可能有一個父菜單:菜單和菜單是自引用關系

在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 錄入數(shù)據(jù)

models.Role.objects.create(name='CEO')
models.Role.objects.create(name='總監(jiān)')
models.Role.objects.create(name='經(jīng)理')
models.Role.objects.create(name='業(yè)務員')

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='總監(jiān)').permissions.add(1,2,5,6)
models.Role.objects.get(name='經(jīng)理').permissions.add(1,5)
models.Role.objects.get(name='業(yè)務員').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用來執(zhí)行初始化權限的操作:用戶登錄后妨退,取出其權限及所屬菜單信息,寫入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數(shù)據(jù)結構如下
    {
        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 數(shù)據(jù)結構如下
    [
        {'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': '菜單二'}
    ]
    """
    #初步處理數(shù)據(jù)碧注,在自定義標簽中生成左側菜單數(shù)據(jù)
    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逝变,在其中實現(xiàn)檢查用戶權限,控制訪問:

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作展開顯示弥臼,其余菜單折疊宴咧;
  • 第三,菜單的層級是不確定的(而且径缅,后面要實現(xiàn)權限的后臺管理掺栅,允許管理員添加菜單和權限);
    在rabc應用的目錄下新建templatetags目錄纳猪,寫一個腳本my_tags.py氧卧,寫一個函數(shù)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 數(shù)據(jù)結構
    [
        {'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 數(shù)據(jù)結構
    {
        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數(shù)據(jù)結構:
    {
        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 # 錄入數(shù)據(jù)
- 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.*',
]

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末氏堤,一起剝皮案震驚了整個濱河市沙绝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖闪檬,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件星著,死亡現(xiàn)場離奇詭異,居然都是意外死亡粗悯,警方通過查閱死者的電腦和手機虚循,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來为黎,“玉大人邮丰,你說我怎么就攤上這事∶” “怎么了剪廉?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長炕檩。 經(jīng)常有香客問我斗蒋,道長,這世上最難降的妖魔是什么笛质? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任泉沾,我火速辦了婚禮,結果婚禮上妇押,老公的妹妹穿的比我還像新娘跷究。我一直安慰自己,他們只是感情好敲霍,可當我...
    茶點故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布俊马。 她就那樣靜靜地躺著,像睡著了一般肩杈。 火紅的嫁衣襯著肌膚如雪柴我。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天扩然,我揣著相機與錄音艘儒,去河邊找鬼。 笑死夫偶,一個胖子當著我的面吹牛界睁,可吹牛的內容都是我干的。 我是一名探鬼主播索守,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼晕窑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了卵佛?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎截汪,沒想到半個月后疾牲,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡衙解,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年阳柔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚓峦。...
    茶點故事閱讀 40,021評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡舌剂,死狀恐怖,靈堂內的尸體忽然破棺而出暑椰,到底是詐尸還是另有隱情霍转,我是刑警寧澤,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布一汽,位于F島的核電站避消,受9級特大地震影響,放射性物質發(fā)生泄漏召夹。R本人自食惡果不足惜岩喷,卻給世界環(huán)境...
    茶點故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望监憎。 院中可真熱鬧纱意,春花似錦、人聲如沸鲸阔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽隶债。三九已至腾它,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間死讹,已是汗流浹背瞒滴。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赞警,地道東北人妓忍。 一個月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像愧旦,于是被迫代替她去往敵國和親世剖。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,974評論 2 355

推薦閱讀更多精彩內容