模型:rbac 基于角色的权限访问控制
什么是权限?
一个包含正则表达式的url就是权限
第一个版本,实现简单的权限验证需求
表结构:
class UserInfo(models.Model):
name=models.CharField(max_length=32)
pwd=models.CharField(max_length=32)
roles=models.ManyToManyField("Role")
def __str__(self):
return self.name
class Role(models.Model):
title=models.CharField(max_length=32)
permissions=models.ManyToManyField("Permission")
def __str__(self):
return self.title
class Permission(models.Model):
title=models.CharField(verbose_name="权限名称",max_length=32)
url=models.CharField(max_length=32)
def __str__(self):
return self.title
URL
url(r'^login/', views.LoginView.as_view()),
url(r'^userinfo/$', views.UserView.as_view()),
url(r'^userinfo/add', views.adduser),
url(r'^userinfo/(\d+)/change/', views.change),
Login(注册session):
views:
from rbac.models import UserInfo
from django.views import View
class LoginView(View):
def get(self,request):
return render(request,"login.html")
def post(self,request):
user=request.POST.get("user")
pwd=request.POST.get("pwd")
#查询是否有这个账户
user=UserInfo.objects.filter(name=user,pwd=pwd).first()
#如果用户存在
if user:
# 将当前登录用户的权限列表注册到session中
request.session["user_id"]=user.pk
permission_list = []
#查询这个用户所有的权限(通过用户-角色-权限),并去重
permissions=user.roles.all().values("permissions__url").distinct()
for per in permissions:
#将这个用户的权限添加到权限列表
permission_list.append(per.get("permissions__url"))
print("permission_list: ",permission_list)
#将权限列表写入session中
request.session["permission_list"]=permission_list
return HttpResponse("OK")
注册中间件
settings
"rbac.apps.RbacConfig"
中间件
rbac.py
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse,render,redirect
class PermissionValid(MiddlewareMixin):
def process_request(self,request):
#获取当前访问路径
current_path = request.path # /userinfo/add/
#设置白名单
white_list=["/login/","/admin/*"]
#如果访问路径在白名单中则放行
for per in white_list:
import re
ret=re.search(per,current_path)
if ret:
return None
#认证用户是否登录
#从session中获取用户ID
user_id=request.session.get("user_id")
#如果用户没有登录则跳转到登录页面
if not user_id:
return redirect("/login/")
#校验权限(第一种方式)
#从session中获取用户权限列表
permission_list = request.session["permission_list"]
import re
flag = False
for permission in permission_list: # "/userinfo/"
#确定校验规则
permission = "^%s$" % permission
ret = re.search(permission, current_path)
if ret:
return None
return HttpResponse("您没有权限访问!")
在使用正则验证权限的时候要注意在确定校验规则时别忘了在路径前后加上^和$
校验包含三个部分:
白名单校验(正则)
登录校验
权限校验
前端页面的校验
前端页面验证当前登录用户是否有登录权限,如果有就显示相应选项
{% if '/userinfo/add/' in permission_list %}
添加用户
{% endif %}
{% for user in user_list %}
{{ user.name }}
{% if "/userinfo/(\d+)/delete/" in permission_list %}
删除
{% endif %}
{% if "/userinfo/(\d+)/change/" in permission_list %}
编辑
{% endif %}
{% endfor %}
思考:上面的操作已经实现权限控制的需求,通过url来控制按钮的显示,但是判断权限使用的条件是写死的,例如/userinfo/add/,如果能让代码更通用更简洁?
第二版
变更数据结构:
添加PermissionGroup表,用于存储权限分组
权限表中添加一个action字段,里面保存add,delete之类的描述行为的信息
权限表中添加一个group字段,与PermissionGroup 表为一对多关系,比如用户表的增删改查权限全规到用户组,角色表的增删改查全归到角色组
修改后的数据结构
class User(models.Model):
name=models.CharField(max_length=32)
pwd=models.CharField(max_length=32)
roles=models.ManyToManyField(to="Role")
def __str__(self): return self.name
class Role(models.Model):
title=models.CharField(max_length=32)
permissions=models.ManyToManyField(to="Permission")
def __str__(self): return self.title
class Permission(models.Model):
title=models.CharField(max_length=32)
url=models.CharField(max_length=32)
action=models.CharField(max_length=32,default="")
group=models.ForeignKey("PermissionGroup",default=1)
def __str__(self):return self.title
class PermissionGroup(models.Model):
title = models.CharField(max_length=32)
def __str__(self): return self.title
url
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^users/$', views.users),
url(r'^users/add', views.add_user),
url(r'^users/delete/(\d+)', views.del_user),
url(r'^roles/', views.roles),
url(r'^login/', views.login),
]
注册session
将注册session功能单独提取出来,与rbac模块放到一起
perssions.py
def initial_session(user,request):
#获取权限url、权限组id、权限action,并去重
permissions = user.roles.all().values("permissions__url","permissions__group_id","permissions__action").distinct()
#将获取到的permissions数据重新组合,构建一个字典,变为我们需要的数据格式
permission_dict={}
for item in permissions:
gid=item.get('permissions__group_id')
if not gid in permission_dict:
permission_dict[gid]={
"urls":[item["permissions__url"],],
"actions":[item["permissions__action"],]
}
else:
permission_dict[gid]["urls"].append(item["permissions__url"])
permission_dict[gid]["actions"].append(item["permissions__action"])
#将转换后的数据写到session中
request.session['permission_dict']=permission_dict
中间件(不要忘了注册到settings.py中)
rbac.py
class ValidPermission(MiddlewareMixin):
def process_request(self,request):
#当前访问路径
current_path = request.path_info
#检查是否属于白名单
valid_url_list=["/login/","/reg/","/admin/.*"]
for valid_url in valid_url_list:
ret=re.match(valid_url,current_path)
if ret:
return None
#校验是否登录
user_id=request.session.get("user_id")
if not user_id:
return redirect("/login/")
#校验权限2
permission_dict=request.session.get("permission_dict")
for item in permission_dict.values():
urls=item['urls']
for reg in urls:
reg="^%s$"%reg
ret=re.match(reg,current_path)
if ret:
#如果匹配成功,就给request添加一个actions
request.actions=item['actions']
return None
return HttpResponse("没有访问权限!")
视图views
from rbac.models import *
#封装增删改查四种权限的判断语句,模板可以直接调用这个类下的属性即可
class Per(object):
def __init__(self,actions):
self.actions=actions
def add(self):
return "add" in self.actions
def delete(self):
return "delete" in self.actions
def edit(self):
return "edit" in self.actions
def list(self):
return "list" in self.actions
def users(request):
user_list=User.objects.all()
#查询当前登录人得名字
id=request.session.get("user_id")
user=User.objects.filter(id=id).first()
#调用上面的Per类,这样在模板中if per.delete就等同于 if "delete" in self.actions,让代码变得更简洁了
per = Per(request.actions)
return render(request,"users.html",locals())
def add_user(request):
return HttpResponse("add user.....")
def del_user(request,id):
return HttpResponse("del"+id)
def roles(request):
role_list=Role.objects.all()
per = Per(request.actions)
return render(request,"roles.html",locals())
from rbac.service.perssions import initial_session
def login(request):
if request.method=="POST":
user=request.POST.get("user")
pwd=request.POST.get("pwd")
user=User.objects.filter(name=user,pwd=pwd).first()
if user:
############################### 在session中注册用户ID######################
request.session["user_id"]=user.pk
###############################在session注册权限列表##############################
#查询当前登录用户的所有权限,注册到session中
initial_session(user,request)
return HttpResponse("登录成功!")
return render(request,"login.html")
模板
举例:
{% if per.add %}
添加用户
{% endif %}
附录:注册session时的数据转换
在上面initial_session中permissions获取到的是一个QuerySet,其格式为:
我们需要把它转换为我们想要的格式,为此我们需要构建一个字典:
{1:
{
'urls':
['/stark/crm/customer/',
'/stark/crm/customer/add/',
'/stark/crm/customer/(\\d+)/change/',
'/stark/crm/customer/public/',
'/stark/crm/customer/mycustomer/'
],
'actions':
['list',
'add',
'edit',
'list',
'list']
}
2:
{
'urls': ['/roles/'],
'actions': ['list']}
}
}
方法1:
#将获取到的permissions重新组合,变为我们需要的数据格式
permission_dict={}
for item in permissions:
gid=item.get('permissions__group_id')
if not gid in permission_dict:
permission_dict[gid]={
"urls":[item["permissions__url"],],
"actions":[item["permissions__action"],]
}
else:
permission_dict[gid]["urls"].append(item["permissions__url"])
permission_dict[gid]["actions"].append(item["permissions__action"])
扩展
侧边菜单的权限控制
菜单栏的显示
举例菜单栏在页面左侧,当前登录用户有几个权限就应该显示几行内容,例如当前用户有查看学生信息的权限,那么菜单中就应该出现学生信息的链接,点击链接可以跳转到相应页面,因此是否显示某一个链接只需要判断当前用户对这个页面是否有list权限即可,那么如何做呢?
在上面的注册权限信息到session阶段,我们构建了一个名为permission_dict的字典,但permission_dict并不方便用于菜单栏显示的判断,因此我们在initial_session函数中再构建一个menu_permission_list列表或元祖来为我们左侧菜单栏的权限判断做服务
代码:
同样在perssions.py的initial_session函数里面
#注册菜单权限
permissions = user.roles.all().values("permissions__url", "permissions__action", "permissions__title").distinct()
menu_permission_list = []
for item in permissions:
if item["permissions__action"] == "list":
menu_permission_list.append((item["permissions__url"], item["permissions__title"]))
print("menu_permission_list==>", menu_permission_list)
request.session["menu_permission_list"] = menu_permission_list
获取"permissions__url", "permissions__action", "permissions__title"三个数据
permissions__action用于判断是否是list
如果是的话将对应的permissions__url和permissions__title存入session
其中permissions__url用于构建a标签中的href,permissions__title用于构建a标签中的文本
menu_permission_list注册的内容如下:
[
('/stark/crm/customer/', '客户管理组'),
('/stark/crm/customer/public/', '客户管理组'),
('/stark/crm/customer/mycustomer/', '客户管理组')
]
模板代码
{% for item in menu_permission_list %}
{% endfor %}
rbac组件的解耦
为了方便使用rbac组件,同时有利于解耦,可以在rbac目录下新建一个templates目录,将menu.html等相关模板放入到这个templates目录下,这样就把每个app自己的模板都放到了同一个app目录下。
找的到吗?
在view中return一个模板通常会到主目录的templates中取寻找相应模板,但如果没找到则会到每个app目录下的名为templates的目录中去寻找这个html模板
多个app的templates有同名模板怎么办?
会按照app注册优先顺序查找,先找到哪个就返回哪个,其它app下同名文件也相同
为了避免这个问题,可以在app的templates目录下再新建一个目录,将模板放入这个目录,例如rbac\templates\rbac\menu.html
在视图中return一个模板时就要写“rbac/users.html”
提示:侧边栏可以使用inclusion_tag来实现