djangopython-Django

Django基于RBAC的权限组件

2018-06-05  本文已影响481人  shu_ke

Django基于RBAC的权限组件


RBAC前奏

  1. RBAC概念
    RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。
  2. 扩展
  1. 基于rbac的实现
    flask-rbac
    simple-rbac

知识点储备

  1. Django ORM
    常用的orm方法,all,values,values_list等
    from models import User
  1. 一对多及多对多
from django.db import models
class A(models.Model):
    name = models.CharField(max_length=32)

class B(models.Model):
    title = models.CharField(max_length=32)
    fk = models.ForeignKey(to="A")
# 跨表操作
a. all()
b_list = models.B.objects.all()
for item in b_list:
    item.id
    item.name
    item.fk_id
    item.fk
    item.fk.name
    item.fk.id
b. values()
b_list = models.B.objects.values('id','name','fk_id','fk__name')
for item in b_list:
    item['id']
    item['name']
    item['fk_id']
    item['fk__name']
c. values_list()
b_list = models.B.objects.values_list('id','name','fk_id','fk__name')
for item in b_list:
    item[0]  # id
    item[1]  # name
    item[2]  # fk_id
    item[3]  # fk__name
d. 查找名称是"Jack"的用户所有B表中的数据
models.B.objects.filter(fk__name="Jack").all()    
from django.db import models
class A(models.Model):
    name = models.CharField(max_length=32)

class B(models.Model):
    title = models.CharField(max_length=32)
    m2m = models.ManyToMany(to="A")
PS: 自动会生成第3张表
a. 在A和B表中各插入2条数据
models.A.objects.create(name="Jack")
models.A.objects.create(name="Mary")

models.A.objects.create(title="IT")
models.A.objects.create(title="CTO")

b. CTO和['Jack','Mary']创建关系
obj = models.B.objects.get(title="CTO")
obj.m2m.add(1)   # 此处可以写id也可以写关联的A表中的obj
obj.m2m.add(2)

c. 查找CTO的关联的人
obj = models.B.objects.get(title="CTO")
obj.m2m.all()    # 得到一个QuerySet列表,内容为A表中的对象
  1. 中间件
    中间件其实就是一个类,包含2个方法,形如:
class MiddleWare:
    # 所有的resquest请求都需要经过该方法,且该方法返回值为None时,继续请求下一个中间件
    def process_request(self,request):
        pass
    def process_response(self,request,response):
        pass

注: 中间件编写完成后需要在settings文件中进行注册使用,注册时注意中间件顺序

  1. Session与Cookie的区别
  1. 正则模块re
    re.match()方法
    决定RE是否在字符串刚开始的位置匹配,返回_sre.SRE_Match对象,如果不能匹配返回None。
    注:这个方法并不是完全匹配。当pattern结束时若string还有剩余字符,仍然视为成功。想要完全匹配,可以在表达式末尾加上边界匹配符'$'
格式:
re.match(pattern, string, flags=0)

print(re.match('com','comwww.runcomoob').group())
print(re.match('com','Comwww.runcomoob',re.I).group())
执行结果如下:
com
com

</br>

RBAC实现

开发RBAC流程

  1. 表结构设计
  2. Django Admin录入数据
  3. 用户登陆
    • 获取角色
    • 获取权限
    • 对权限URL进行去重
  4. 生成权限结构信息,写入session中
{
    1: {
        'urls': ['/userinfo/', '/userinfo/add/', '/userinfo/(\\d+)/delete/', '/userinfo/(\\d+)/change/'],
        'codes': ['list', 'add', 'del', 'edit']
    },
    2: {
        'urls': ['/order/', '/order/add/', '/order/(\\d+)/delete/', '/order/(\\d+)/change/'],
        'codes': ['list', 'add', 'del', 'edit']
    }
}
  1. 注册中间件
    • 白名单
    • 获取当前访问url: request.path_info
    • session中获取权限,进行权限访问验证
  2. 自动生成菜单功能
    • 采用自定义tag方式实现(inclusion_tag)
    • 作为模板使用{% menu_html request %}方式导入html文件中使用
  3. 通过Django Admin后台进行管理及维护工作

Django ORM表结构设计

5个类6张表

  1. 菜单表
class Menu(models.Models):
    """
    菜单表
    """
    title = models.CharField(max_length=32,verbose_name='菜单标题')
    
     # django admin后台显示用
    class Meta:
        verbose_name_plural = "菜单表"
    # 重写__str__方法,实例化后的对象将以字符串的形式展示,但实际是一个obj,所以,请不要相信你的眼睛,必要时使用type(arg)进行验证
    def __str__(self):
        return self.title
  1. 权限组表
class Group(models.Model):
   """
   权限组
   """
   caption = models.CharField(max_length=32, verbose_name="组名称")
   menu = models.ForeignKey(to="Menu", default=1, blank=True, verbose_name="关联的菜单")

   class Meta:
       verbose_name_plural = "权限组"

   def __str__(self):
       return self.caption
  1. 权限表
class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(max_length=32, verbose_name="标题")
    url = models.CharField(max_length=128, verbose_name="含正则的URL")
    # menu_gp为null说明是title为菜单项
    menu_gp = models.ForeignKey(to="Permission", null=True, blank=True, verbose_name="默认选中的组内权限ID", related_name="pm")
    code = models.CharField(max_length=16, verbose_name="权限码")
    group = models.ForeignKey(to="Group", blank=True, verbose_name="所属组")

    class Meta:
        verbose_name_plural = "权限表"

    def __str__(self):
        return self.title
  1. 用户表
class User(models.Model):
    """
    用户表
    """
    username = models.CharField(max_length=32, verbose_name="用户名")
    password = models.CharField(max_length=64, verbose_name="密码")
    email = models.CharField(max_length=32, verbose_name="邮箱")
    roles = models.ManyToManyField(to="Role", blank=True, verbose_name="用户关联的角色")

    class Meta:
        verbose_name_plural = "用户表"

    def __str__(self):
        return self.username
  1. 角色表
class Role(models.Model):
    """
    角色表
    """
    title = models.CharField(max_length=32, verbose_name="角色名称")
    permissions = models.ManyToManyField(to="Permission", blank=True, verbose_name="角色关联的权限")

    class Meta:
        verbose_name_plural = "角色表"

    def __str__(self):
        return self.title
  1. 附加

</br>

settings中添加配置项

在文件末尾添加配置信息

vim projectname/settings.py
# ########################### 权限管理相关 ###########################3
PERMISSION_MENU_KEY = "asdkjalsdf9uajsdf"
PERMISSION_URL_DICT_KEY = "iujmsufnsdflsdkf"

VALID_URL= [
    '^/login/',
    "^/admin*"
]

初始化权限信息

#cat rbac/service/init_permission.py

from django.conf import settings

def init_permission(request,user):
    """
    用户权限信息初始化,获取当前用户所有权限信息,并保存到Session中
    此处的request以及user参数均为对象,user为登陆成功时在数据库中查询到的user对象
    :param request:
    :param user:
    :return:
    """
    # 去空去重
    permission_list = user.roles.filter(permissions__id__isnull=False).values(
        'permissions__id',
        'permissions__title',                # 用户列表
        'permissions__url',
        'permissions__code',
        'permissions__menu_gp_id',           # 组内菜单ID,Null表示是菜单
        'permissions__group_id',             # 权限的组ID
        'permissions__group__menu_id',       # 当前权限所在组的菜单ID
        'permissions__group__menu__title',   # 当前权限所在组的菜单名称
    ).distinct()
    
    # 菜单相关配置,在inclusion_tag中使用
    menu_permission_list= []
    for item in permission_list:
        tpl = {
            'id': item['permissions__id'],
            'title': item['permissions__title'],
            'url': item['permissions__url'],
            'menu_gp_id': item['permissions_menu_gp_id'],
            'menu_id': item['permissions__group__menu_id'],
            'menu_title': item['permissions__group__menu__title'] 
        }
        menu_permission_list.append(tpl)
        request.session[settings.PERMISSION_MENU_KEY] = menu_permission_list
        # 形如
        """
        {"url": "/host/","menu_title": "主机管理","title": "主机列表","id": 1,"menu_gp_id": null,"menu_id": 1},
        {"url": "/host/add/","menu_title": "主机管理","title": "添加主机","id": 2,"menu_gp_id": 1,"menu_id": 1},
        {"url": "/host/(\\d+)/delete/","menu_title": "主机管理","title": "删除主机","id": 3,"menu_gp_id": 1,"menu_id": 1},
        {"url": "/host/(\\d+)/change/","menu_title": "主机管理","title": "修改主机","id": 4,"menu_gp_id": 1,"menu_id": 1}
        {"url": "/userinfo/","menu_title": "用户管理","title": "用户列表","id": 5,"menu_gp_id": null,"menu_id": 2},
        {"url": "/userinfo/add/","menu_title": "用户管理","title": "添加用户","id": 6,"menu_gp_id": 5,"menu_id": 2},
        ......
        """

    # 权限相关,中间件使用
    permission_dict = {}
    for item in permission_list:
        group_id = item['permissions__group_id']
        code = item['permissions__code']
        url = item['permissions__url']
        if group_id in permission_dict:
            permission_dict[group_id]['codes'].append(code)
            permission_dict[group_id]['urls'].append(url)
        else:
            permission_dict[group_id] = {"codes": [code, ], "urls": [url, ]}
    request.session[settings.PERMISSION_URL_DICT_KEY] = permission_dict
    # 形如
    """
    {
        "1": {
            "codes": ["list","add","delete","edit"],
            "urls": ["/host/","/host/add/","/host/(\\d+)/delete/","/host/(\\d+)/change/"]
         },
        "2": {
            "codes": ["list","add","delete","change"],
            "urls": ["/userinfo/","/userinfo/add/","/userinfo/(\\d+)/delete/","/userinfo/(\\d+)/change/"]
         }
    }
    """

注: 用户登陆成功后进行初始化权限信息,在处理用户权限时需要进行数据去重

菜单List及权限Dict格式如下所示:

# 菜单List request.session[settings.PERMISSION_MENU_KEY]
[
    {
        "url": "/host/",
        "menu_title": "主机管理",
        "title": "主机列表",
        "id": 1,
        "menu_gp_id": null,
        "menu_id": 1
    },
    {
        "url": "/host/add/",
        "menu_title": "主机管理",
        "title": "添加主机",
        "id": 2,
        "menu_gp_id": 1,
        "menu_id": 1
    },
    {
        "url": "/host/(\\d+)/delete/",
        "menu_title": "主机管理",
        "title": "删除主机",
        "id": 3,
        "menu_gp_id": 1,
        "menu_id": 1
    },
    {
        "url": "/host/(\\d+)/change/",
        "menu_title": "主机管理",
        "title": "修改主机",
        "id": 4,
        "menu_gp_id": 1,
        "menu_id": 1
    },
    {
        "url": "/userinfo/",
        "menu_title": "用户管理",
        "title": "用户列表",
        "id": 5,
        "menu_gp_id": null,
        "menu_id": 2
    },
    {
        "url": "/userinfo/add/",
        "menu_title": "用户管理",
        "title": "添加用户",
        "id": 6,
        "menu_gp_id": 5,
        "menu_id": 2
    },
    {
        "url": "/userinfo/(\\d+)/delete/",
        "menu_title": "用户管理",
        "title": "删除用户",
        "id": 7,
        "menu_gp_id": 5,
        "menu_id": 2
    },
    {
        "url": "/userinfo/(\\d+)/change/",
        "menu_title": "用户管理",
        "title": "修改用户",
        "id": 8,
        "menu_gp_id": 5,
        "menu_id": 2
    }
]

# 权限Dict request.session[settings.PERMISSION_URL_DICT_KEY]
{
    "1": {
        "codes": [
            "list",
            "add",
            "delete",
            "edit"
        ],
        "urls": [
            "/host/",
            "/host/add/",
            "/host/(\\d+)/delete/",
            "/host/(\\d+)/change/"
        ]
    },
    "2": {
        "codes": [
            "list",
            "add",
            "delete",
            "change"
        ],
        "urls": [
            "/userinfo/",
            "/userinfo/add/",
            "/userinfo/(\\d+)/delete/",
            "/userinfo/(\\d+)/change/"
        ]
    }
} 

</br>

中间件

cat rbac/middleware/rbac.py

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

# 在后续版本中可能会被废弃,故在此直接引入
#from django.utils.deprecation import MiddlewareMixin
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):
        # 1. 当前请求URL
        current_request_url = request.path_info

        # 2. 处理白名单,如login及admin页面需开放访问权限,根据实际情况而定
        for url in settings.VALID_URL_LIST:
            if re.match(url,current_request_url):
                return None

        # 3. 获取session中保存的权限信息
        permission_dict = request.session.get(settings.PERMISSION_MENU_LIST)
        if not permission_dict:
            # 登陆页面
            return redirect(settings.RBAC_LOGIN_URL)

        flag = False
        for group_id, values in permission_dict.items():
            for url in values['urls']:
                regex = settings.URL_FORMAT.format(url)
                if re.match(regex, current_request_url):
                    flag = True
                    break
            if flag:
                break
        if not flag:
            # 无权访问页面,可以直接redirect
            return HttpResponse('无权访问')

</br>

自动生成菜单template tags

  1. template tags部分
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# __author__ = "shuke"
# Date: 2017/11/20

from django.conf import settings
from django.template import Library
import re
import json

register = Library()


@register.inclusion_tag('menu.html')
def menu_html(request):
    """
    获取session中的菜单信息,匹配当前URL,生成菜单
    :param request: 请求的requst对象
    :return:
    """
    menu_list = request.session.get(settings.PERMISSION_MENU_KEY)
    # 当前请求URL
    current_url = request.path_info

    menu_dict = {}
    # menu_gp_id为空则是菜单
    for item in menu_list:
        if not item['menu_gp_id']:
            menu_dict[item['id']] = item

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

    result = {}
    for item in menu_dict.values():
        active = item.get('active')
        menu_id = item['menu_id']
        if menu_id in result:
            result[menu_id]['children'].append({'title': item['title'], 'url': item['url'], 'active': active})
            if active:
                result[menu_id]['active'] = True
        else:
            result[menu_id] = {
                'menu_id': item['menu_id'],
                'menu_title': item['menu_title'],
                'active': active,
                'children': [
                    {'title': item['title'], 'url': item['url'], 'active': active}
                ]
            }
    print(json.dumps(result, indent=4, ensure_ascii=False))
    return {'menu_dict': result}
  1. 生成的菜单树格式如下
{
    "1": {
        "children": [
            {
                "url": "/host/",
                "active": null,
                "title": "主机列表"
            }
        ],
        "menu_id": 1,
        "menu_title": "主机管理",
        "active": null
    },
    "2": {
        "children": [
            {
                "url": "/userinfo/",
                "active": null,
                "title": "用户列表"
            }
        ],
        "menu_id": 2,
        "menu_title": "用户管理",
        "active": null
    }
}
  1. menu_tpl.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 %}
  1. HTML部分使用tags
# 上文中的menu_html函数依赖request参数,此处需要传入
{% load rbac %}
{% menu_html request %}

注: 自定义tags只支持传入1个参数
</br>

注册中间件使用

project/settings.py

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'rbac.middleware.rbac.RbacMiddleware',

]

维护

在Django Admin中维护rbac的权限系统并使用

总结

至此,基于role实现的rbac组件基本开发完成,在Django中作为app引入在settings文件中注册后就可以生效使用了,engoy it!

上一篇 下一篇

猜你喜欢

热点阅读