rbac简介
项目的GitHub地址
欢迎Download&Fork&Star:https://github.com/Wanghongw/CombineRbac
另外,本文只简单介绍一下rbac权限组件在实际开发中存在的必要以及详细介绍它是如何与实际项目结合的,至于什么是rbac组件以及rbac组件的实现方式本文不会涉及。
rbac权限组件的引入
关于“权限”,先给大家举一个“大闹天宫”的一个场景来理解:话说这天玉帝老儿要举行蟠桃大会,参会的有各路神仙纷纷入席。像托塔天王、太上老君这等上仙不仅可以获得一个VIP席位,更有吃桃子的权利;而下一级别的小仙们只能站着看嫦娥跳舞了!其实这也还好,可笑那“孙猴弼马温”,连参会的资格都没得,只能在马圈里跟马度日。
其实“权限”这个概念不仅在小说中存在,在我们实际中它一直围绕在我们工作与生活的始终。
比如我们在进入一些技术交流网站的时候,普通用户每天会限制只能看几篇文章,但是如果你充值成为VIP会员后,你每天浏览的文章数量不仅不会有限制而且还提供下载到本地服务!这就是“权限”在实际中最常用到的地方!
如果你是一名程序开发者,可以毫不夸张的说:在你的编程生涯中应该会有百分之八十的代码会跟“权限”打交道——是不是引起了你的强烈共鸣呢?2333333
权限组件的正式引入
实际中我们经常会做一些类似于这样的一个简单的增删改查项目:
从演示中的效果可以看出来,对博客与文章每个登陆的用户都有增删改查的权利!
但是,做完后你的产品经理不满意了:我规定只有管理员可以增删改,其他人登陆后只能查看,而且,不能给他们显示添加、编辑与删除这些按钮!
听完这个需求后,聪明的你顿时想到了一个方案:我给不同权限的登陆角色分配不同的页面就好了嘛!于是你按照产品经理的意思又在项目中加了许多页面——不久之后,你突然意识到前面给自己挖了一个大坑:随着项目功能与用户角色的不断增加,你在项目中增加的页面也越来越多!
通过给不同的角色分配不同页面的解决方式会给项目中增加许多的“冗余功能与代码”,这是我们在实际的开发中要坚决杜绝的!
于是,我们急需这样一个权限组件:它既可以帮我们实现“不同角色的用户进入项目后拥有的操作权限不一样”,而且我们不需要再在既有的项目中添加额外冗余的页面或代码——只需要将这个权限组件引入到项目后进行一些必要的配置后即可使用!
项目结合权限组件后的效果与说明
github中的那两个项目一个是没有加rbac组件的原始项目,一个是与rbac组件合并后的项目。通过观察你可以发现其实整合后的项目似乎只比之前的项目多了一个叫rbac的应用,配置文件似乎也多了一些内容,之前做的原始的项目似乎并没有再额外增加什么文件!这就是rbac组件的一个方便之处:帮你在节省代码的前提下实现了你想要的功能。
先来看一下rbac组件实现的功能效果:
管理员用户有所有的访问权限:
一般高级用户只有业务的访问权限:
权限最低的用户只有查看的权限,没有操作的权限:
rbac与实际项目整合过程的详细说明
前提概要
Python与django版本
python==3.6.5
django==1.11.20
项目中每条路有都必须有name别名
项目中的每条路由都必须有name值!否则rbac组件无法正常运行!
因为两个功能用到了那么别名:一个是“权限分配时需要找到项目中的所有路由”,另外一个是“权限粒度控制到按钮级别”也用到了name。最重要的是后台的数据结构也是用name的值来构建的。
因此,如果想用本rbac组件请检查一下你的项目中所有路由是否都配置了name别名。
否则会出现下面这样的异常:
关于用户Model的属性名的说明
强烈提示大家把业务应用中用户Model的用户名与密码属性值跟rbac应用中用户Model的用户名与密码的属性值保持统一!
至于原因后面会有说明。
外部脚本调用Django环境统一插入测试数据
也许你还在为测试数据的录入感到烦恼。我这里为大家提供了一个脚本,可以快速的在blog应用中插入数据,帮助大家测试——这个insert_data.py文件看整合后的项目的那个:
# -*- coding:utf-8 -*- import os import random import string if __name__ == '__main__': os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_add_rbac.settings") import django django.setup() # 插入数据 from blog import models # 往博客表中插入数据 blog_lst = ['火之国','水之国','风之国','雷之国','土之国'] name_lst = ['whw','wanghw','naruto','sasuke','madara'] b_lst = [] for bid,blog in enumerate(blog_lst): blog_obj = models.Blog(name=blog,user=name_lst[bid]) b_lst.append(blog_obj) models.Blog.objects.bulk_create(b_lst) # 往文章表中插入数据 category_choices = [4,2,3] dates = ['2011-12-12','2012-3-5','2018-12-5'] blog_id_lst = [1,2,3,4,5] lst = [] # 插入50个数据 for i in range(50): random_title = ''.join(random.choices(string.ascii_letters, k=5)) random_category = random.choice(category_choices) random_content = ''.join(random.choices(string.ascii_letters, k=9)) random_date = random.choice(dates) random_blog = random.choice(blog_id_lst) customer_obj = models.Article(title=random_title,category=random_category,content=random_content,create_at=random_date,blog_id=random_blog) lst.append(customer_obj) models.Article.objects.bulk_create(lst)
将rbac组件(应用)拷贝到项目中
这一步大家应该都会:将rbac组件原封不动的拷贝到项目的根目录下,以后把它作为我们项目的一个应用。
下面我会将rbac称为“应用”而不是“组件”,因为这时候它已经是我们项目中的一部分了!
settings中进行配置
settings中的配置主要就是注册rbac应用及其中间件,另外就是配置rbac应用用到的白名单等等必要参数。
注册rbac应用到项目中
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'blog.apps.BlogConfig', # 注册rbac应用 'rbac.apps.RbacConfig', ]
注册rbac应用的中间件
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组件的中间件 'rbac.middlewares.rbac.RbacMiddleware', ]
将rbac应用需要的配置文件写在settings中
之所以有这一步,是因为我在进行rbac组件开发的时候用的就是当时那个项目中的settings的相关配置,如果把rbac移到其他地方的话,应用中必要用到的settings中的参数也必须跟我源代码中的一样。否则会上报KeyError异常。
################# rbac组件 相关的配置 ################# # 业务中的用户表 # 用于在rbac分配权限时,读取业务表中的用户信息。 # 注意这里是你业务代码中用户表的路径 RBAC_USER_MODLE_CLASS = "blog.models.UserInfo" # 需要登录但无需权限的URL NO_PERMISSION_LIST = [ r'/blog/index/$', # '/blog/logout/', ] # 白名单,无需登录就可以访问 VALID_URL_LIST = [ r'^/$', r'/blog/login/$', r'/blog/admin/.*' ] # 自动化发现路由中URL时,排除的URL AUTO_DISCOVER_EXCLUDE = [ r'/admin/.*', r'/blog/login/$', # r'/blog/logout/', r'/blog/index/$', ] # 存放权限信息的session_key PERMISSION_SESSION_KEY = "permission_url_list_key" # 存放菜单信息的session_key MENU_SESSION_KEY = "permission_menu_key"
在项目的“总路由”加上rbac应用的分发
from django.conf.urls import url,include from django.contrib import admin from blog.views import auth urlpatterns = [ url(r'^admin/', admin.site.urls), # 访问根目录直接去blog的login页面 url(r'^$',auth.login,name='straight_login'), # blog应用的分发 url(r'^blog/',include(('blog.urls','blog'))), # rbac应用的分发 url(r'^rbac/',include(('rbac.urls','rbac'))), ]
静态文件的整理
实际中在写项目的时候我习惯上这样去定义静态文件的路径以及静态文件存放的目录名:
第一个STATIC_URL是模板中引用的时候用到的名字,是settings默认的配置,默认叫static;
第二个STATICFILES_DIRS是项目中存放静态文件的文件夹的名字,是我自己手动填上去的,由于我们之前在进行项目部署的时候遇到过将它命名为“static”的一个坑, 因此我现在习惯上把这个目录起名为staticfiles。
但是,rbac这个组件的静态文件存放目录我之前给它命名为static了,结合的时候需要注意什么呢?
也许看图更能说明问题:
实际中我选用了第一种方式:将rbac组件用到的静态文件统一放在了项目的staticfiles目录中了:
模板文件的整理
模板文件的整理也是需要注意的!因为我之前做rbac组件的时候用的模板的母版文件是base.htnl,但是这个项目中的母版文件是layout.html。因此rbac其他的模板文件的母版发生变化了!应当继承新的母版文件!
最后提醒大家2点:
1、一定要记得在新的母版中引入rbac应用用到的js与css文件(jquery与BootStrap也是必须的)!否则rbac相关的模板效果是出不来的!
2、rbac组件用到inclusion_tag的地方(动态生成二级菜单以及路径导航)一定要记得写在新的母版中去!
Model的整合及数据库的迁移
几乎所有的程序都会把登陆作为自己项目的入口的,因此我这里默认大家的项目都有登陆功能,并且登陆一定会用到用户名与密码至少两个字段。
rbac组件的核心就是“用户——角色——权限”,通过给用户指定角色,为角色绑定不同的权限去实现“权限分配”功能,所以通过登陆获取用户的权限是十分关键的一个环节!
拿我这个项目来讲,我这个项目用到了登陆认证功能,而且用户的Model是:
# blog应用的用户Model
class UserInfo(models.Model): """ 用户信息 """ name = models.CharField(max_length=33,verbose_name='用户名',unique=True,default=None) password = models.CharField(max_length=33,verbose_name='密码') email = models.EmailField(verbose_name='邮箱',blank=True,null=True)
但是,不巧的是我的rbac应用也有一个用户Model,并且是这样定义的(跟blog的用户Model一样!):
# rbac组件的用户Model 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(verbose_name='拥有的所有角色', to=Role, blank=True,null=True) def __str__(self): return self.name
针对上述这种情况,如果rbac应用中有与项目中功能相同的表(实际上我现在遇到的所有情况都是用户表重复的),应该进行“功能合并”处理。
所谓的功能合并,其实就是不影响既有功能(权限校验功能与业务功能,用户表的业务功能基本上都是用来做登陆校验的)的前提下将两张表“合并”成一张表!
实现的方式就是将rbac的那个用户Model作为“基类”,让业务的用户Model去继承它:
rbac的用户Model作为基类,这样修改:
# rbac组件的用户Model class UserInfo(models.Model): """ 用户表 """ # 将rbac的用户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) # 作为"基类"的话~to后面不可以跟字符串形式的Role,必须是类~~因为在业务的用户Model中无法用反射取到Role~~ roles = models.ManyToManyField(verbose_name='拥有的所有角色', to=Role, blank=True,null=True) # def __str__(self): # return self.name class Meta: # django以后再做数据库迁移时,不再为UserInfo类创建相关的表以及表结构了。 # 此类可以当做"父类",被其他Model类继承。 abstract = True
blog应用的用户Model作为子类去继承rbac的用户Model,相关代码如下:
from rbac.models import UserInfo as RbacUser # 继承rbac的用户Model class UserInfo(RbacUser): """ 用户信息 """ name = models.CharField(max_length=33,verbose_name='用户名',unique=True,default=None) password = models.CharField(max_length=33,verbose_name='密码')
通过这种方式,在进行数据库迁移的时候是不会生成rbac的用户表的,并且blog的用户Model还跟rbac应用的角色Model有ManyToMany的关系(因为blog的用户Model继承自rbac的用户Model)—— 这一点大家一定要切记!
这里还需要额外补充一个大家可能会忽略的点:如果业务的用户Model是用auth组件做的~因此作为基类的rbac的用户Model有跟auth_user表重名的属性也必须注释掉!比如邮箱等等~~
关于业务应用中用户Model的用户名与密码字段的特别提示
之所以做这样一个友情提示,是因为我在实际遇到了相关的问题后才对这个“看似不起眼的属性名问题”引起了足够的重视!
因为我做rbac组件的时候用户名与密码用的是name与password,因此组件中所有与这两个有关的代码(主要是做表单校验与视图函数中用到了)也都是这两个名字,因此,我这里强烈提示大家把业务应用中用户Model的用户名与密码属性值跟rbac应用中用户Model的用户名与密码的属性值保持统一!
如果你没有按照我说的去做的话,那么下面这个错误可能会在比较长的一段时间内伴随着你:
我这里的解决方案是把业务应用中用户Model的用户名与密码都改成了name与password,跟rbac的保持一致——当然,如果你的业务应用在其他地方也用到了这些属性名,那就要自己去取舍了——这里提示大家一点:我在rbac应用的表单校验与视图函数中用到了name与password值,大家也可以在这两个地方把他们修改成跟自己项目一样的属性名
rbac应用中用到用户Model的地方需要修改一下导入的模块
这一点也十分重要!因为rbac应用之前是用自己的用户Model的,现在自己的用户Model作为基类被新的应用继承,我们应该在rbac应用中用到用户Model的地方修改一下导入的模块,主要在两个地方:
一个是在做表单校验的地方修改一下:
另外一个是在rbac的视图函数中:
数据库的迁移
上面这些你如果都做完后,那么不出什么意外的话就可以执行数据库迁移指令了“
python3 manage.py makemigrations
python3 manage.py migrate
我的这个项目一共生成了8张表,可以看到,用户表与角色表之间多对多关系的第三张表也生成了,说明前面Model配置的没问题并且数据库迁移也无误!
启动项目开始调试
接下来到了最激动人心的调试环节了!
(1)测试是否能顺利进入登陆界面
如果在注册了rbac的应用与中间件的情况下能进入项目的登陆界面~说明前面的操作是没问题的!
如果出现了未获得权限的提示,请查看一下settings中设置的白名单,你的login的路由是不是写的正确!
如果出现小黄页~那就要看具体的报错来进行调试了!
这里特别说明一点:调试的过程请不要跳步骤,可能你自己的程序在跟我的rbac组件结合的时候会产生各种各样的报错,那么这个时候请你先把每个错误排查完以后再进行下面的操作!不要给自己挖坑!
(2)注释掉中间件及inclusion_tag进行调试
如果你顺利通过了(1)的试炼,那么现在请先把rbac的中间件注释掉,然后把母版文件中用到inclusion_tag的地方也注释掉!因为现在我们没有任何操作的权限!
(3)访问/rbac/user/list/进行用户管理
访问/rbac/user/list/路径进行用户的增删改查操作——请忽略左侧的之前固定生成的菜单。
(4)访问/rbac/role/list/进行角色管理
访问/rbac/role/list/路径进行角色的增删改查操作:——请忽略左侧的之前固定生成的菜单。
(5)访问/rbac/menu/list/进行权限的批量操作
访问/rbac/menu/list/路径进行菜单及权限的具体分配:——请忽略左侧的之前固定生成的菜单。
点击上图右上方的“批量操作”按钮可以对权限进行批量操作:
当然具体的分配得根据实际需求来:我这个项目用到了2级动态菜单,因此需要给每个“一级菜单”下面分配“二级菜单”,二级菜单下面的“子菜单”其实是实现“点击子菜单权限”保留对应二级菜单样式的功能的——如果你也做了相似的项目一定会对这个功能的印象十分深刻!2333333
(6)访问/rbac/distribute/permissions/进行权限分配
在此之前还需要提醒大家一点:看一下自己的settings配置中是否有读取业务中的用户信息的相关配置:
# 业务中的用户表 # 用于在rbac分配权限时,读取业务表中的用户信息。 RBAC_USER_MODLE_CLASS = "blog.models.UserInfo"
下面是我进行权限分配的效果图:
(7)在登陆逻辑中加上权限注入的逻辑
在项目的登陆逻辑中加入权限的注入逻辑。
我这里通过rbac组件的一个 init_permission 函数实现的权限的“注入”。
所谓的“权限注入”,说白了就是通过获取到当前登陆的用户对象,利用ORM拿到这个用户所拥有的权限(url)以及其他我们需要的数据,然后进行数据结构的构建,把构建好的数据结构存入sessin中,在中间件中进行校验或者在自定义的标签及过滤器中用这些数据构建inclusion_tag或者自定义的过滤器!
项目的登陆逻辑修改如下:
from django.shortcuts import render from django.http import JsonResponse from blog import models from rbac.service.init_permission import init_permission def login(request): data = {'code': None, 'msg': None} if request.method == 'GET': return render(request,'blog/login.html') elif request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') # 用项目的用户Model做校验 user_obj = models.UserInfo.objects.filter(name=username,password=password).first() print(user_obj,type(user_obj)) if user_obj: # 权限注入 init_permission(user_obj,request) request.session['is_login'] = True request.session['user_id'] = user_obj.pk request.session['user'] = user_obj.name data['code']=1000 return JsonResponse(data) else: data['code'] = 2000 data['msg'] = '用户名或密码错误' return JsonResponse(data) def index(request): return render(request,'blog/index.html') # return redirect('blog:index')
(8)取消中间件与inclusion_tag的注释进行权限组件的测试
进行完前7步后,这里我们可以将之前注释掉的rbac的中间件以及母版中的inclusion_tag放开了。
项目中用到inclusion_tag的地方就两个:一个是动态的左侧二级菜单,一个就是路径导航栏。
然后,记得把之前固定生成左侧菜单的那几个标签注释掉, 以免影响动态二级菜单的生成。
(9)权限粒度控制到按钮级别的实现
这一步是实现“不同的权限看到不同的操作页面”的关键——当然,你得保证前面几个步骤完全执行无误后再进行接下来的这步操作!
再次提醒大家:一定要保证项目中的所有url都有一个name别名!
在rbac组件中我自定义了一个过滤器,用来判断当前登陆的用户是否有相应的操作权限,如果有的话就给他显示对应权限的按钮或者标签,如果他没有相应的权限的话页面中就不给他显示对应的标签。
拿添加文章这个按钮举例:
(10)最终效果演示
管理员用户:
一般高级用户:
权限最低的用户:
写在最后
在平时的工作中要学会积累——将一些通用的组件或功能:分页、权限、批量对数据库进行增删改查的组件等等都积累到自己的”技术存储站“中~~用的时候直接改吧改吧套进去就ok了!这样可以大大的提高自己的开发效率!