基于角色的权限管理
权限组件
在我们写项目时,可能会遇到给不同的用户分配不同的权限的情况,那么什么是权限呢?权限其实就是一个url
不同的url代表不同的功能,限定用户能访问的url,就给了用户不同的权限
权限管理在很多项目中都有用到,所以我们可以讲权限管理的逻辑写成一个组件
使它在不同的项目中只要经过一定的修改就能使用
创建项目
在一个项目中可以包含多个组件,同样一个组件也可以用于多个项目,这里我们想创建一个项目
然后创建两个app,一个app01写项目的主逻辑,一个rbac(Role-Based Access Control)写权限相关的逻辑

建表
我们首先能想到的是用户和权限表,一个用户可以有多个权限,而一个权限也可以对应多个用户,这样他们就是多对多的关系
但是这样的话,我们会发现一个问题,比如一个公司有很多销售员,这些销售员都有同样的权限,这时我们就需要添加很多重复的信息
这里我们可以再添加一张角色表,让角色和用户、权限都有多对多的关系
这样有新的用户后,只要给该用户分配一个角色就行了,而该角色又拥有相应的权限,就不需要再添加重复的信息了
rbac.models
from django.db import models
# Create your models here.
class UserInfo(models.Model):
name = models.CharField(max_length=32)
pwd = models.CharField(max_length=32, default=123)
email = models.EmailField()
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):
url = models.CharField(max_length=32)
title = models.CharField(max_length=32)
def __str__(self):
return self.title
表建好后我们就要往表中添加数据了,这里我们可以先用admin添加一些基本数据
rbac.admin
from django.contrib import admin
from .models import *
# Register your models here.
admin.site.register(UserInfo)
admin.site.register(Role)
class PermissionConfig(admin.ModelAdmin):
list_display = ["id", "title", "url"]
ordering = ["id"]
admin.site.register(Permission, PermissionConfig)
一般情况下,我们添加一条数据会看到一个数据对象,如果想要展示具体的id,title等属性,我们可以使用上面的方法,创建一个新的类继承(admin.ModelAdmin),在里面定义list_display,讲想要看到的内容写入列表,然后使用admin.site.register(Permission, PermissionConfig),这样我们就可以在页面上看到如下效果

这里我们添加了8个权限,我们可以发现这8个权限可以分为两组,前4个是和用户有关的,而后4个是和订单有关的
这里我们可以再创建一张权限组的表,使他和权限表一对多关联,同时给权限表再创建一个编号
rbac.models
from django.db import models
# Create your models here.
class UserInfo(models.Model):
name = models.CharField(max_length=32)
pwd = models.CharField(max_length=32, default=123)
email = models.EmailField()
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):
url = models.CharField(max_length=32)
title = models.CharField(max_length=32)
permission_group = models.ForeignKey("PermissionGroup", default=1)
code = models.CharField(max_length=32, default="")
def __str__(self):
return self.title
class PermissionGroup(models.Model):
caption = models.CharField(max_length=32)
def __str__(self):
return self.caption
rbac.admin
from django.contrib import admin
from .models import *
# Register your models here.
admin.site.register(UserInfo)
admin.site.register(Role)
admin.site.register(PermissionGroup)
class PermissionConfig(admin.ModelAdmin):
list_display = ["id", "title", "url", "permission_group", "code"]
ordering = ["id"]
admin.site.register(Permission, PermissionConfig)
最后添加的权限如下

用户登录
urls
from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^login/', views.login),
url(r'^users/', views.users),
url(r'^orders/', views.orders),
url(r'^orders/add/', views.orders_add),
]
这里我们为一些相关权限也设置了url
视图函数
由于这些是关于项目的逻辑,所以视图函数写在app01.views
from django.shortcuts import render, redirect, HttpResponse
from rbac import models
# Create your views here.
def login(request):
if request.method == "GET":
return render(request, "login.html")
else:
user = request.POST.get("user")
pwd = request.POST.get("pwd")
user = models.UserInfo.objects.filter(name=user, pwd=pwd).first()
if user:
# 验证成功之后
request.session["user_id"] = user.pk
# 当前登录用户的所有权限
from rbac.service.initial import initial_session
initial_session(request, user)
return HttpResponse("登录成功")
else:
return redirect("/login/")
def users(request):
return HttpResponse("用户列表")
def orders(request):
permission_dict = request.session.get("permission_dict")
return render(request, "orders.html", locals())
def orders_add(request):
return HttpResponse("添加订单")
login.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="x-ua-compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登录</title>
</head>
<body>
<form action="/login/" method="post">
{% csrf_token %}
<p>用户名 <input type="text" name="user"></p>
<p>密码 <input type="password" name="pwd"></p>
<input type="submit" value="提交">
</form>
</body>
</html>
可以看到当用户登录以后我们先验证账号密码是否正确,然后写session,这里我们调用了rbac内的方法
from rbac.service.initial import initial_session initial_session(request, user)
这个方法究竟做了什么呢

def initial_session(request, user):
# 方式1
# permission_info = user.roles.all().values("permissions__url", "permissions__title").distinct()
# temp = []
# for i in permission_info:
# temp.append(i["permissions__url"])
# request.session["permission_list"] = temp
# 方式2
# 创建一个数据格式:包含所有权限,权限所在组,权限的编号
permission_info = user.roles.all().values("permissions__url", "permissions__code", "permissions__permission_group_id").distinct()
permission_dict = {}
for permission in permission_info:
if permission["permissions__permission_group_id"] in permission_dict:
permission_dict[permission["permissions__permission_group_id"]]["urls"].append(
permission["permissions__url"])
permission_dict[permission["permissions__permission_group_id"]]["codes"].append(
permission["permissions__code"])
else:
permission_dict[permission["permissions__permission_group_id"]] = {
"urls": [permission["permissions__url"]],
"codes": [permission["permissions__code"]]
}
'''
permission_dict = {
1:{},
2:{
"urls": [],
"codes": []
}
}
'''
request.session["permission_dict"] = permission_dict
我们可以看到这个方法有两种定义session数据的方式,方式1只是简单的取出数据库中用户拥有权限的url,并添加到一个列表中
而方式2定义了一个如注释中所见的数据形式,将相关数据写入session中后,当用户访问一个url时,我们就可以从session中取出该用户拥有的权限,再验证一下访问的url是否在用户的权限中,如果在,那么就让他通过,不在则返回无权访问
我们发现,不论用户访问哪个url我们都应该做权限的验证,这时就需要使用中间件来解决验证问题了
中间件

from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import render, redirect, HttpResponse
import re
class M1(MiddlewareMixin):
def process_request(self, request):
current_path = request.path_info
# 白名单,当用户访问以下url时直接通过
valid_url_menu = ["/login/", "/reg/", "/admin/.*"]
for valid_url in valid_url_menu:
ret = re.match(valid_url, current_path)
if ret:
return None
# 方式1
# permissions_list = request.session.get("permission_list")
# 方式2
permission_dict = request.session.get("permission_dict")
if not request.session.get("user_id"):
return redirect("/login/")
for item in permission_dict.values():
regs = item["urls"]
codes = item["codes"]
for reg in regs:
reg = "^%s$" % reg
ret = re.match(reg, current_path)
if ret:
request.permission_codes = codes
return None
return HttpResponse("无权访问")
# 方式1
# flag = False
# for permission_url in permissions_list:
# permission_url = "^%s$" % permission_url
# ret = re.match(permission_url, current_path)
# if ret:
# flag = True
# break
# if not flag:
# return HttpResponse("无权访问")
当用户访问时,先取到用户访问的url:request.path_info
有些url我们应该让每个用户都能访问,比如登录、注册页面等
所以我们设置一个白名单,如果用户访问的url在白名单内,则直接通过
如果不在,我们从request.session中取到用户相关的权限,根据我们设置的数据类型,验证用户访问的url是否在权限内
这里要注意,上面我们提到权限是一个url,这里我们还要补充一下,权限是一个包含正则的url,所以在验证是,我们用到了re模块进行正则匹配,同时为了配置的精准,我们在权限前后分别加上了^和$符
当匹配成功后我们将url对应的code列表存到request中,并在中间件中放行
页面上数据的使用
如果用上面的方式1存数据,那么我们能拿到的只是一个url的列表,其中还包含正则表达式,在页面上使用时我们不能使用正则匹配,所以无法进行判断
所以我们使用方式2的数据
视图函数
def orders(request):
permission_dict = request.session.get("permission_dict")
permission_codes = request.permission_codes
per = Permissions(permission_codes)
return render(request, "orders.html", locals())
这里的Permissions是我们自己定义的一个类,类中定义了判断增删改查是否在permission_codes中的方法,方便我们在前端使用
class Permissions(object):
def __init__(self, code_list):
self.code_list = code_list
def list(self):
return "list" in self.code_list
def add(self):
return "add" in self.code_list
def delete(self):
return "delete" in self.code_list
def edit(self):
return "edit" in self.code_list
前端页面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="x-ua-compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Title</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>
<h3>订单列表</h3>
<div class="col-md-6">
{% if per.add %}
<p><a href="/orders/add/"><button class="btn btn-primary pull-right">添加订单</button></a></p>
{% endif %}
<table class="table table-stripped">
<tr>
<th>订单号</th>
<th>订单日期</th>
<th>商品名称</th>
<th>操作</th>
</tr>
<tr>
<td>12343</td>
<td>2012-12-12</td>
<td>li</td>
{% if per.delete %}
<td><a href=""><button class="btn btn-danger btn-sm">删除</button></a></td>
{% endif %}
</tr>
</table>
</div>
</body>
</html>
前端我们只需要使用per这个对象的方法就能直接判断当前用户有没有对应的权限,从而进行页面渲染
权限菜单
上面的方法我们通过判断用户的权限,在页面上给用户显示响应的按钮
现在我们又有了新的需求,要在页面上显示一个左侧菜单,菜单中有不同的菜单栏,每一个菜单栏中都对应有用户有的相应权限
当用户访问某一个url时,url对应的权限会变色,同时该权限所在的菜单栏会是打开状态,其它菜单栏是关闭状态
如果通过我们之前的表结构设计,这个菜单栏对用的其实就是权限组,这时会有一个问题,当我们显示权限菜单时,删除和编辑权限所对应的url是一个正则表达式,这样的权限其实我们是不应该显示在菜单栏的
在展示时我们需要通过判断将这些权限给排除,这个过程其实比较简单,但是我们要考虑,当我们访问这些权限时,权限菜单中的哪个权限应该变色呢,我觉得应该是删除和编辑对应的展示列表权限应该变色
这时我们会发现我们每一个菜单栏其实就是一个权限组,而里面对应的权限其实很少,这种情况下当权限组较多时,我们的菜单也会很多
这种二级菜单的显示效果就不是很好了,所以我们对表结构进行一些修改
rbac.models
from django.db import models
# Create your models here.
class Menu(models.Model):
caption = models.CharField(max_length=32)
def __str__(self):
return self.caption
class UserInfo(models.Model):
name = models.CharField(max_length=32)
pwd = models.CharField(max_length=32, default=123)
email = models.EmailField()
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):
url = models.CharField(max_length=32)
title = models.CharField(max_length=32)
permission_group = models.ForeignKey("PermissionGroup", default=1)
code = models.CharField(max_length=32, default="")
parent = models.ForeignKey("self", default=1, null=True, blank=True)
def __str__(self):
return self.title
class PermissionGroup(models.Model):
caption = models.CharField(max_length=32)
menu = models.ForeignKey("Menu", default=1)
def __str__(self):
return self.caption
我们增加了一个菜单表,该表一对多对应权限组表,同时我们在权限表中增加了一个新的字段parent,这个字段自关联自己,当某个权限不需要在菜单栏展示,我们就给他一个parent,这样访问他时,对应的parent权限在菜单栏就会变色
修改了表结构以后,我们的菜单、权限组和权限现在是一个三级菜单的关系
菜单
权限组
权限
我们展示时只展示菜单和权限,不考虑权限组,这时我们需要考虑的是需要拿到怎样的数据在页面上进行展示
menu_dict = {
1: {
"title": "菜单一",
"active": False,
"children": [
{"title": "添加用户", "url": "xxxxxxxxxxx", "active": False},
{"title": "查看用户", "url": "xxxxxxxxxxx", "active": False},
]},
2: {
"title": "菜单二",
"active": True,
"children": [
{"title": "添加用户", "url": "xxxxxxxxxxx", "active": False},
{"title": "查看用户", "url": "xxxxxxxxxxx", "active": True},
]
}}
上面的数据中,字典的key(1、2)表示的是菜单的id,active表示的是菜单是否是打开状态,children列表中放的是菜单拥有的权限,权限的active表示的是权限是否变色
有了这样的数据我们可以通过下面的方法在页面上进行渲染
<div class="menu">
{% for item in menu_dict.values %}
<div class="item">
<div class="title"><a href="">{{ item.title }}</a></div>
{% if item.active %}
<div class="con">
{% else %}
<div class="con hide">
{% endif %}
{% for son in item.children %}
{% if son.active %}
<p><a href="{{ son.url }}" class="active">{{ son.title }}</a></p>
{% else %}
<p><a href="{{ son.url }}">{{ son.title }}</a></p>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="content">
{% block con %}
{% endblock %}
</div>
</div>
现在的问题就是我们要获得这样的数据类型,首先用户登录时,我们可以先从数据库中取出我们需要的数据,放到session中
修改rbac.initial.py
def initial_session(request, user):
# 方式1
# permission_info = user.roles.all().values("permissions__url", "permissions__title").distinct()
# temp = []
# for i in permission_info:
# temp.append(i["permissions__url"])
# request.session["permission_list"] = temp
# 方式2
# 创建一个数据格式:包含所有权限,权限所在组,权限的编号
permission_info = user.roles.all().values("permissions__url", "permissions__code", "permissions__permission_group_id").distinct()
permission_dict = {}
for permission in permission_info:
if permission["permissions__permission_group_id"] in permission_dict:
permission_dict[permission["permissions__permission_group_id"]]["urls"].append(
permission["permissions__url"])
permission_dict[permission["permissions__permission_group_id"]]["codes"].append(
permission["permissions__code"])
else:
permission_dict[permission["permissions__permission_group_id"]] = {
"urls": [permission["permissions__url"]],
"codes": [permission["permissions__code"]]
}
'''
permission_dict = {
1:{},
2:{
"urls": [],
"codes": []
}
}
'''
request.session["permission_dict"] = permission_dict
# 创建生成菜单的数据
permission_info = user.roles.all().values("permissions__url", "permissions__code",
"permissions__permission_group_id",
"permissions__parent_id",
"permissions__permission_group__menu__id",
"permissions__permission_group__menu__caption",
"permissions__title",
"permissions__id").distinct()
permission_list = []
for permission_item in permission_info:
temp = {
"id": permission_item["permissions__id"],
"url": permission_item["permissions__url"],
"title": permission_item["permissions__title"],
"pid": permission_item["permissions__parent_id"],
"menu_name": permission_item["permissions__permission_group__menu__caption"],
"menu_id": permission_item["permissions__permission_group__menu__id"]
}
permission_list.append(temp)
request.session["permission_list"] = permission_list
上面的permission_list是我们取到的原始数据,现在我们要将该数据处理成我们理想的结构
def get_menu(request):
permission_list = request.session.get("permission_list")
# 存储所有放到菜单栏中的权限
temp_dict = {}
for item in permission_list:
pid = item["pid"]
if not pid:
item["active"] = False
temp_dict[item["id"]] = item
# 将需要标中的active设置为True
current_path = request.path_info
for item in permission_list:
pid = item["pid"]
url = "^%s$" % item["url"]
if re.match(url, current_path):
if pid:
temp_dict[pid]["active"] = True
else:
item["active"] = True
# 将temp_dict转换为最终的menu_dict的数据格式
menu_dict = {}
for item in temp_dict.values():
if item["menu_id"] in menu_dict:
menu_dict[item["menu_id"]]["children"].append(
{"title": item["title"], "url": item["url"], "active": item["active"]})
if item["active"]:
menu_dict[item["menu_id"]]["active"] = True
else:
menu_dict[item["menu_id"]] = {
"title": item["menu_name"],
"active": item["active"],
"children": [
{"title": item["title"], "url": item["url"], "active": item["active"]}
]
}
return {"menu_dict": menu_dict}
首先将需要展示的权限(parent_id为None的)统一取到temp_dict字典中,然后根据用户访问的url修改权限的active值
最后将temp_dict转换成我们需要的结构,同时根据权限的active确定菜单的active,这样我们就得到了我们需要的数据
但是,这里我们又发现一个问题,每次我们访问不同的url,到达不同的视图函数时都需要这样处理一遍数据,十分麻烦
我们发现其实我们访问的每一个页面都有这样的权限菜单,这就让我们想到了模板继承和我们之前用过的自定义标签

from django import template
import re
register = template.Library()
@register.inclusion_tag("menu.html")
def get_menu(request):
permission_list = request.session.get("permission_list")
# 存储所有放到菜单栏中的权限
temp_dict = {}
for item in permission_list:
pid = item["pid"]
if not pid:
item["active"] = False
temp_dict[item["id"]] = item
# 将需要标中的active设置为True
current_path = request.path_info
for item in permission_list:
pid = item["pid"]
url = "^%s$" % item["url"]
if re.match(url, current_path):
if pid:
temp_dict[pid]["active"] = True
else:
item["active"] = True
# 将temp_dict转换为最终的menu_dict的数据格式
menu_dict = {}
for item in temp_dict.values():
if item["menu_id"] in menu_dict:
menu_dict[item["menu_id"]]["children"].append(
{"title": item["title"], "url": item["url"], "active": item["active"]})
if item["active"]:
menu_dict[item["menu_id"]]["active"] = True
else:
menu_dict[item["menu_id"]] = {
"title": item["menu_name"],
"active": item["active"],
"children": [
{"title": item["title"], "url": item["url"], "active": item["active"]}
]
}
return {"menu_dict": menu_dict}
页面模板
menu.html
<div class="menu">
{% for item in menu_dict.values %}
<div class="item">
<div class="title"><a href="">{{ item.title }}</a></div>
{% if item.active %}
<div class="con">
{% else %}
<div class="con hide">
{% endif %}
{% for son in item.children %}
{% if son.active %}
<p><a href="{{ son.url }}" class="active">{{ son.title }}</a></p>
{% else %}
<p><a href="{{ son.url }}">{{ son.title }}</a></p>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
base.html
{% load my_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Title</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
<style>
.header {
width: 100%;
height: 50px;
background-color: #336699;
}
.menu, .content {
float: left;
}
.menu {
width: 200px;
height: 600px;
background-color: darkgray;
}
.hide {
display: none;
}
.menu .title {
font-size: 16px;
color: #336699 !important;
margin: 20px 0;
}
.con a {
margin-left: 30px;
color: white;
}
.active {
color: red !important;
}
</style>
</head>
<body>
<div class="header"></div>
<div class="box">
<div class="row">
{% get_menu request %}
<div class="content col-md-9">
{% block con %}
{% endblock %}
</div>
</div>
</div>
</body>
</html>
orders.html
{% extends "base.html" %}
{% block con %}
<h3>订单列表</h3>
<div class="col-md-6">
{% if per.add %}
<p><a href="/orders/add/"><button class="btn btn-primary pull-right">添加订单</button></a></p>
{% endif %}
<table class="table table-stripped">
<tr>
<th>订单号</th>
<th>订单日期</th>
<th>商品名称</th>
<th>操作</th>
</tr>
<tr>
<td>12343</td>
<td>2012-12-12</td>
<td>li</td>
{% if per.delete %}
<td><a href=""><button class="btn btn-danger btn-sm">删除</button></a></td>
{% endif %}
</tr>
</table>
</div>
{% endblock %}
其它的页面也都继承该模板
最后的视图函数
from django.shortcuts import render, redirect, HttpResponse
from rbac import models
from rbac.service.base import *
import re
# Create your views here.
def login(request):
if request.method == "GET":
return render(request, "login.html")
else:
user = request.POST.get("user")
pwd = request.POST.get("pwd")
user = models.UserInfo.objects.filter(name=user, pwd=pwd).first()
if user:
# 验证成功之后
request.session["user_id"] = user.pk
# 当前登录用户的所有权限
from rbac.service.initial import initial_session
initial_session(request, user)
return HttpResponse("登录成功")
else:
return redirect("/login/")
class UserPermissions(Permissions):
def xxx(self):
return "xxx" in self.code_list
def users(request):
return render(request, "users.html", locals())
def orders(request):
permission_dict = request.session.get("permission_dict")
permission_codes = request.permission_codes
per = Permissions(permission_codes)
return render(request, "orders.html", locals())
def orders_add(request):
return HttpResponse("添加订单")
def m1(request):
return render(request, "m1.html")
def m2(request):
return render(request, "m2.html")
这样访问/orders/时,我们看到简单的效果了


浙公网安备 33010602011771号