概述

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

在实际应用中,url是依附在菜单下的,比如一个简单的生产企业管理系统,菜单可以大致分为以下几块:制造、资材、生产管理、人事、财务等等。每个菜单下又可以有子菜单,但最终都会指向一个url,点击这个url,通过Django路由系统执行一个视图函数,来完成某种操作。这里,制造部的员工登录系统后,肯定不能点击财务下的菜单,甚至都不会显示财务的菜单。

设计表关系

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

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

其中角色和权限、用户和角色,是两个多对多关系,由Django自动生成另外两种关联表。因此一共会产生6张表,用来实现权限管理。

下面我们新建一个项目,并在项目下新建rbac应用,在该应用的models.py中来定义这几张表:

from django.db import modelsclass Menu(models.Model):"""菜单"""title = models.CharField(max_length=32, unique=True)parent = models.ForeignKey("Menu", null=True, blank=True) # 定义菜单间的自引用关系# 权限url 在 菜单下;菜单可以有父级菜单;还要支持用户创建菜单,因此需要定义parent字段(parent_id)# blank=True 意味着在后台管理中填写可以为空,根菜单没有父级菜单def __str__(self):# 显示层级菜单title_list = [self.title]p = self.parentwhile p:title_list.insert(0, p.title)p = p.parentreturn '-'.join(title_list)class Permission(models.Model):"""权限"""title = models.CharField(max_length=32, unique=True)url = models.CharField(max_length=128, unique=True)menu = models.ForeignKey("Menu", null=True, blank=True)def __str__(self):# 显示带菜单前缀的权限return '{menu}---{permission}'.format(menu=self.menu, permission=self.title)class Role(models.Model):"""角色:绑定权限"""title = models.CharField(max_length=32, unique=True)permissions = models.ManyToManyField("Permission")# 定义角色和权限的多对多关系def __str__(self):return self.titleclass UserInfo(models.Model):"""用户:划分角色"""username = models.CharField(max_length=32)password = models.CharField(max_length=64)nickname = models.CharField(max_length=32)email = models.EmailField()roles = models.ManyToManyField("Role")# 定义用户和角色的多对多关系def __str__(self):return self.nickname

权限的初始化和验证

我们知道Http是无状态协议,那么服务端如何判断用户是否具有哪些权限呢?通过session会话管理,将请求之间需要”记住“的信息保存在session中。用户登录成功后,可以从数据库中取出该用户角色下对应的权限信息,并将这些信息写入session中。

所以每次用户的Http request过来后,服务端尝试从request.session中取出权限信息,如果为空,说明用户未登录,重定向至登录页面。否则说明已经登录(即权限信息已经写入request.session中),将用户请求的url与其权限信息进行匹配,匹配成功则允许访问,否则拦截请求。

我们先来实现第一步:提取用户权限信息,并写入session

为了实现rabc功能可在任意项目中的可用,我们单独创建一个rbac应用,以后其它项目需要权限管理时,直接拿到过,稍作配置即可。在rbac应用下新建一个文件夹service,写一个脚本init_permission.py用来执行初始化权限的操作:用户登录后,取出其权限及所属菜单信息,写入session中

from ..models import UserInfo, Menudef init_permission(request, user_obj):"""初始化用户权限, 写入session:param request: :param user_obj: :return: """permission_item_list = user_obj.roles.values('permissions__url','permissions__title','permissions__menu_id').distinct()permission_url_list = []  # 用户权限url列表,--> 用于中间件验证用户权限permission_menu_list = []  # 用户权限url所属菜单列表 [{"title":xxx, "url":xxx, "menu_id": xxx},{},]for item in permission_item_list:permission_url_list.append(item['permissions__url'])if item['permissions__menu_id']:temp = {"title": item['permissions__title'],"url": item["permissions__url"],"menu_id": item["permissions__menu_id"]}permission_menu_list.append(temp)menu_list = list(Menu.objects.values('id', 'title', 'parent_id'))# 注:session在存储时,会先对数据进行序列化,因此对于Queryset对象写入session,加list()转为可序列化对象from django.conf import settings  # 通过这种方式导入配置,具有可迁移性# 保存用户权限url列表request.session[settings.SESSION_PERMISSION_URL_KEY] = permission_url_list# 保存 权限菜单 和所有 菜单;用户登录后作菜单展示用request.session[settings.SESSION_MENU_KEY] = {settings.ALL_MENU_KEY: menu_list,settings.PERMISSION_MENU_KEY: permission_menu_list,}

可以在项目的settings中指定session保存权限信息的key:

# 定义session 键:
# 保存用户权限url列表
# 保存 权限菜单 和所有 菜单
SESSION_PERMISSION_URL_KEY = 'cool'SESSION_MENU_KEY = 'awesome'
ALL_MENU_KEY = 'k1'
PERMISSION_MENU_KEY = 'k2'

这样,用户登录后,调用init_permission,即可完成初始化权限操作。而且即使修改了用户权限,每次重新登录后,调用该方法,都会更新权限信息:

from django.shortcuts import render, redirect, HttpResponse
from rbac.models import UserInfo
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_obj = UserInfo.objects.filter(username=username, password=password).first()if not user_obj:return render(request, "login.html", {'error': '用户名或密码错误!'})else:init_permission(request, user_obj) #调用init_permission,初始化权限return redirect('/index/')

第二步,检查用户权限,控制访问

要在每次请求过来时检查用户权限,对于这种对请求作统一处理的需求,利用中间件再合适不过(关于中间件的信息,可以参考我的另一篇博文)。我们在rbac应用下新建一个目录middleware,用来存放自定义中间件,新建rbac.py,在其中实现检查用户权限,控制访问:

from django.conf import settings
from django.shortcuts import HttpResponse, redirect
import reclass MiddlewareMixin(object):def __init__(self, get_response=None):self.get_response = get_responsesuper(MiddlewareMixin, self).__init__()def __call__(self, request):response = Noneif 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 responseclass RbacMiddleware(MiddlewareMixin):"""检查用户的url请求是否是其权限范围内"""def process_request(self, request):request_url = request.path_infopermission_url = request.session.get(settings.SESSION_PERMISSION_URL_KEY)print('访问url',request_url)print('权限--',permission_url)# 如果请求url在白名单,放行for url in settings.SAFE_URL:if re.match(url, request_url):return None# 如果未取到permission_url, 重定向至登录;为了可移植性,将登录url写入配置if not permission_url:return redirect(settings.LOGIN_URL)# 循环permission_url,作为正则,匹配用户request_url# 正则应该进行一些限定,以处理:/user/ -- /user/add/匹配成功的情况flag = Falsefor url in permission_url:url_pattern = settings.REGEX_URL.format(url=url)if re.match(url_pattern, request_url):flag = Truebreakif flag:return Noneelse:# 如果是调试模式,显示可访问urlif settings.DEBUG:info ='<br/>' + ( '<br/>'.join(permission_url))return HttpResponse('无权限,请尝试访问以下地址:%s' %info)else:return HttpResponse('无权限访问')

说明:

  • 有些访问不需要权限,或者在测试时,我们可以在settings中配置一个白名单;
  • 将登录的url写入settings中,增强可移植性;
  • url本质是正则表达式,在匹配用户请求的url是否在其权限范围内时,需要作严格匹配,这个也可以在settings中配置
  • 中间件定义完成后,加入settings中的MIDDLEWARE列表中最后面(加到前面可能还没有session信息)

settings中的配置如下:

LOGIN_URL = '/login/'REGEX_URL = r'^{url}$'  # url作严格匹配# 配置url权限白名单
SAFE_URL = [r'/login/','/admin/.*','/test/','/index/','^/rbac/',
]MIDDLEWARE = ['django.middleware.security.SecurityMiddleware','......','rbac.middleware.rbac.RbacMiddleware'  # 加入自定义的中间件到最后
]

菜单显示

用户登录后,应该根据其权限,显示其可以操作的菜单。前面我们我们已经将用户的权限和菜单信息保存在了request.session中,因此如何从中提取信息,并将其渲染成页面显示的菜单,就是接下来要解决的问题。

提取信息很简单,因为在用户登录后调用init_permission初始化权限时,已经将权限和菜单信息进行了初步处理,并写入了session,这里只需要通过key将信息取出来即可。

显示菜单要处理三个问题:

  • 第一,只显示用户权限对应的菜单,因此不同用户看到的菜单可能是不一样的
  • 第二,对用户当前访问的菜单下的url作展开显示,其余菜单折叠;
  • 第三,菜单的层级是不确定的(而且,后面要实现权限的后台管理,允许管理员添加菜单和权限);

自定义标签

接下来我们通过自定义标签(关于自定义标签的方法,可以参考我之前的一篇关于模板的博文),来实现以上需求:

  • 它接收request参数,从中提取session保存的权限和菜单数据;
  • 对数据作结构化处理
  • 将数据渲染为html字符串。

下面 我们在rabc应用的目录下新建templatetags目录,写一个脚本custom_tag.py,写一个函数rbac_menu,并加上自定义标签的装饰器:

from django import template
from django.utils.safestring import mark_saferegister = template.Library()def get_structure_data(request):passdef get_menu_html(menu_data):pass@register.simple_tag
def rbac_menu(request):"""显示多级菜单:请求过来 -- 拿到session中的菜单,权限数据 -- 处理数据 -- 作显示数据处理部分抽象出来由单独的函数处理;渲染部分也抽象出来由单独函数处理"""menu_data = get_structure_data(request)menu_html = get_menu_html(menu_data)return mark_safe(menu_html)# 因为标签无法使用safe过滤器,这里用mark_safe函数来实现

其中,我们将数据处理部分和数据渲染部分抽象为两个函数:

数据处理

from django.conf import settings
import re, osdef get_structure_data(request):"""处理菜单结构"""menu = request.session[settings.SESSION_MENU_KEY]all_menu = menu[settings.ALL_MENU_KEY]permission_url = menu[settings.PERMISSION_MENU_KEY]# all_menu = [#     {'id': 1, 'title': '订单管理', 'parent_id': None},#     {'id': 2, 'title': '库存管理', 'parent_id': None},#     {'id': 3, 'title': '生产管理', 'parent_id': None},#     {'id': 4, 'title': '生产调查', 'parent_id': None}# ]# 定制数据结构all_menu_dict = {}for item in all_menu:item['status'] = Falseitem['open'] = Falseitem['children'] = []all_menu_dict[item['id']] = item# all_menu_dict = {#     1: {'id': 1, 'title': '订单管理', 'parent_id': None, 'status': False, 'open': False, 'children': []},#     2: {'id': 2, 'title': '库存管理', 'parent_id': None, 'status': False, 'open': False, 'children': []},#     3: {'id': 3, 'title': '生产管理', 'parent_id': None, 'status': False, 'open': False, 'children': []},#     4: {'id': 4, 'title': '生产调查', 'parent_id': None, 'status': False, 'open': False, 'children': []}# }# permission_url = [#     {'title': '查看订单', 'url': '/order', 'menu_id': 1},#     {'title': '查看库存清单', 'url': '/stock/detail', 'menu_id': 2},#     {'title': '查看生产订单', 'url': '/produce/detail', 'menu_id': 3},#     {'title': '产出管理', 'url': '/survey/produce', 'menu_id': 4},#     {'title': '工时管理', 'url': '/survey/labor', 'menu_id': 4},#     {'title': '入库', 'url': '/stock/in', 'menu_id': 2},#     {'title': '排单', 'url': '/produce/new', 'menu_id': 3}# ]request_rul = request.path_infofor url in permission_url:# 添加两个状态:显示 和 展开url['status'] = Truepattern = url['url']if re.match(pattern, request_rul):url['open'] = Trueelse:url['open'] = False# 将url添加到菜单下all_menu_dict[url['menu_id']]["children"].append(url)# 显示菜单:url 的菜单及上层菜单 status: truepid = url['menu_id']while pid:all_menu_dict[pid]['status'] = Truepid = all_menu_dict[pid]['parent_id']# 展开url上层菜单:url['open'] = True, 其菜单及其父菜单open = Trueif url['open']:ppid = url['menu_id']while ppid:all_menu_dict[ppid]['open'] = Trueppid = all_menu_dict[ppid]['parent_id']# 整理菜单层级结构:没有parent_id 的为根菜单, 并将有parent_id 的菜单项加入其父项的chidren内menu_data = []for i in all_menu_dict:if all_menu_dict[i]['parent_id']:pid = all_menu_dict[i]['parent_id']parent_menu = all_menu_dict[pid]parent_menu['children'].append(all_menu_dict[i])else:menu_data.append(all_menu_dict[i])return menu_data

渲染菜单

多级菜单的显示需要用到递归,因为层级不确定

def get_menu_html(menu_data):"""显示:菜单 + [子菜单] + 权限(url)"""option_str = """<div class='rbac-menu-item'><div class='rbac-menu-header'>{menu_title}</div><div class='rbac-menu-body {active}'>{sub_menu}</div></div>"""url_str = """<a href="{permission_url}" class="{active}">{permission_title}</a>""""""menu_data = [{'id': 1, 'title': '订单管理', 'parent_id': None, 'status': True, 'open': False,'children': [{'title': '查看订单', 'url': '/order', 'menu_id': 1, 'status': True, 'open': False}]},{'id': 2, 'title': '库存管理', 'parent_id': None, 'status': True, 'open': True,'children': [{'title': '查看库存清单', 'url': '/stock/detail', 'menu_id': 2, 'status': True, 'open': False},{'title': '入库', 'url': '/stock/in', 'menu_id': 2, 'status': True, 'open': True}]},{'id': 3, 'title': '生产管理', 'parent_id': None, 'status': True, 'open': False,'children': [{'title': '查看生产订单', 'url': '/produce/detail', 'menu_id': 3, 'status': True, 'open': False},{'title': '排单', 'url': '/produce/new', 'menu_id': 3, 'status': True, 'open': False}]},{'id': 4, 'title': '生产调查', 'parent_id': None, 'status': True, 'open': False,'children': [{'title': '产出管理', 'url': '/survey/produce', 'menu_id': 4, 'status': True, 'open': False},{'title': '工时管理', 'url': '/survey/labor', 'menu_id': 4, 'status': True, 'open': False}]}]"""menu_html = ''for item in menu_data:if not item['status']: # 如果用户权限不在某个菜单下,即item['status']=False, 不显示continueelse:if item.get('url'): # 说明循环到了菜单最里层的urlmenu_html += url_str.format(permission_url=item['url'],active="rbac-active" if item['open'] else "",permission_title=item['title'])else:menu_html += option_str.format(menu_title=item['title'],sub_menu=get_menu_html(item['children']),active="" if item['open'] else "rbac-hide")return menu_html

样式和JS文件处理

在渲染菜单时会用到自定义的css和js文件,这些也应该打包好,保证rbac的可迁移性。因此,在这个自定义标签的脚本中,额外定义两个标签,用来加载css和js文件:

@register.simple_tag
def rbac_css():"""rabc要用到的css文件路径,并读取返回;注意返回字符串用mark_safe,否则传到模板会转义:return: """css_path = os.path.join('rbac', 'style_script','rbac.css')css = open(css_path,'r',encoding='utf-8').read()return mark_safe(css)@register.simple_tag
def rbac_js():"""rabc要用到的js文件路径,并读取返回:return: """js_path = os.path.join('rbac', 'style_script', 'rbac.js')js = open(js_path, 'r', encoding='utf-8').read()return mark_safe(js)

这样,菜单显示就完成了。用户登录后,假如访问index.html页面,那么只要在该模板中调用上面的自定义标签即可:

{% load custom_tag %}
{% load static %}<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
<!-- 通过调用自定义标签中的函数,导入rbac中的css和js --><style>{% rbac_css %}</style><script src="{% static 'jquery-3.2.1.js' %}"></script><script>$(function () {{% rbac_js %}})</script></head>
<body>
<!-- 生成菜单 -->
{% rbac_menu request %}</body>
</html>

权限的后台管理

权限的后台管理,就是提供对Model中定义的那几张表的增删改查功能。这里以用户表UserInfo为例来说明。

路由分发

因为权限管理作为一个单独的模块,所以需要在项目的全局urls.py中作一个路由分发:

from django.conf.urls import url, includeurlpatterns = [url(r'^rbac/', include('rbac.urls') )
]

在rbac应用的urls.py中定义具体的路由:

from django.conf.urls import url
from . import viewsurlpatterns = [url(r'^users/$', views.users),url(r'^users/new/$', views.users_new),url(r'^users/edit/(?P<id>\d+)/$', views.users_edit),url(r'^users/delete/(?P<id>\d+)/$', views.users_delete),url(r'^$', views.index),
]

视图中处理增删改查

定义ModelForm

这里利用Django的ModelForm,简化这些操作(关于ModelForm的使用,可以参考我的博客)。首先在rbac应用的forms.py中定义UserInfo的ModelForm:

from django.forms import ModelForm
from .models import UserInfo, Role, Permission, Menuclass UserInfoModelForm(ModelForm):class Meta:model = UserInfofields = '__all__'labels = {'username': '用户名','password': '密码','nickname': '昵称','email': '邮箱','roles': '角色',}

视图逻辑

这里要注意的就是,如果是修改,那么需要给model_form对象传入一个实例对象。

from django.shortcuts import render, redirect, reverse
from .models import UserInfo, Role, Permission, Menu
from .forms import UserInfoModelForm, RoleModelForm, PermissionModelForm, MenuModelFormdef index(request): # 提供后台管理的入口return render(request, 'rbac/index.html')def users(request):"""查询所有用户信息"""user_list = UserInfo.objects.all()return render(request, 'rbac/users.html', {'user_list': user_list})def users_new(request):if request.method =="GET":# 传入ModelForm对象model_form = UserInfoModelForm()return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '新增用户'})else:model_form = UserInfoModelForm(request.POST)if model_form.is_valid():model_form.save()return redirect(reverse(users))else:return render(request, 'rbac/common_edit.html',{'model_form': model_form, 'title': '新增用户'})def users_edit(request,id):user_obj = UserInfo.objects.filter(id=id).first()if request.method == 'GET':model_form = UserInfoModelForm(instance=user_obj)return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '编辑用户'})else:model_form = UserInfoModelForm(request.POST, instance=user_obj)if model_form.is_valid():model_form.save()return redirect(reverse(users))else:return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '编辑用户'})def users_delete(request, id):user_obj = UserInfo.objects.filter(id=id).first()user_obj.delete()return redirect(reverse(users))

基于Django实现RBAC权限管理相关推荐

  1. laravel学习笔记------使用 Entrust 扩展包在 Laravel 5 中实现 RBAC 权限管理

    为什么80%的码农都做不了架构师?>>>    Entrust为我们在Laravel中实现基于角色的权限管理(RBAC)提供了简洁灵活的方式. 1.安装 想要在Laravel中使用E ...

  2. nest-mysql:RBAC权限管理

    文章问题导向 RBAC权限管理是什么?如何设计数据库?如何实现? 如果你都有了答案,可以忽略本文章,或去nest学习导图寻找更多答案 阅前必知 阅读此文,需要有一定的数据库知识 此文并非最佳实践,只能 ...

  3. RBAC权限管理设计思想

    RBAC权限管理设计 一.概述 二.权限模型 三.RBAC模型 什么是RBAC模型 基本模型RBAC0 角色分层模型RBAC1 角色限制模型RBAC2 统一模型RBAC3 基于RBAC的延展--用户组 ...

  4. RBAC权限管理设计

    RBAC权限管理设计 一.RBAC组成 1. RBAC 2. RBAC组成 3. RBAC支持的安全原则 4. RBAC的优缺点 二.RBAC权限分配 1. RBAC的功能模块 2. RBAC权限分配 ...

  5. php rbac实现,php实现rbac权限管理

    php实现rbac权限管理 介绍: RBAC(Role-Based Access Control)基于角色的权限管理方式. RBAC的最大特征就是将权限跟角色挂钩,用户又跟角色挂钩. 优点: ①管理维 ...

  6. Java Web RBAC权限管理

    前言 权限管理是在项目中经常要使用到的模块,有着极其重要的功能.比较出名的权限框架,分别为 Shiro 和 Spring Security,两者各有优缺,这次我们不用任何权限框架来实现 RBAC 权限 ...

  7. Vue3 实现 RBAC 权限管理

    Vue3 实现 RBAC 权限管理 RBAC的基本概念 RBAC本质上就是一个授权的过程通过 用户 -> 角色---->资源 为啥要用RBAC 一个系统中用户是非常多的,对于不同的用户,展 ...

  8. kubernetes(k8s)之rbac权限管理详解

    kubernetes(k8s)之rbac权限管理详解 RBAC简介 RBAC(Role-Based Access Control) [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 ...

  9. 在Egg.js中实现RBAC权限管理

    什么是RBAC? RBAC是基于角色的权限访问控制,在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限,也就是说权限是和角色绑定在一起的. RBAC权限管理树形图 角色管理 ...

最新文章

  1. fabric-ca-server 配置mysql数据库,区块链(4)
  2. 小说里的lt什么意思_脂肪醇里的脂肪是什么意思
  3. Spring(2)——Spring IoC 详解
  4. JVM 的内存结构和内存分配
  5. Monkey Server自动化脚本
  6. mysql排行榜sql的实现
  7. 【格局视野】三色需求与工作层次
  8. MongoDB与阿里云达成战略合作,最新数据库独家上线阿里云!
  9. zookeeper 和 kafka 集群搭建
  10. Linux io运行情况,Linux IO调度层分析
  11. SVN工作笔记001---svn删除已经上传的文件
  12. 关于servlet中出现GET方法不能应用于此url的解决办法
  13. Java事务管理之JDBC
  14. “Null 是价值十亿美元的错误!”
  15. python中return self用法详解
  16. BootStrap--CSS组件--按钮组(btn-group)
  17. 服务器SN信息,命令查看服务器SN号
  18. 人身三流指什么_什么是“下三流”哪三流,有何解释?
  19. cips2016+学习笔记︱简述常见的语言表示模型(词嵌入、句表示、篇章表示)
  20. 安装linux双系统简书,安装win10+ubuntu18.04双系统

热门文章

  1. 3-6:类与对象下篇——构造函数中的初始化列表、匿名对象和explicit关键字
  2. LeetCode 37 解数独
  3. LeetCode 452 用最少数量的箭引爆气球
  4. 面试题59 - II. 队列的最大值
  5. 1108. IP 地址无效化
  6. MySql 主从模式原理及操作步骤
  7. 分布式网络游戏百万人同时在线服务器架构实现
  8. 查找问题的利器 - Git Bisect
  9. POJ 2186 Popular Cows(Tarjan)
  10. RhinoMock入门(4)——次序和委托