学自:https://www.cnblogs.com/wupeiqi/articles/9178982.html
权限系统介绍
在一个公司有不同的等级,不同等级看到的信息是不同的,这就是权限。如一个项目下的资料只能该项目中成员能看,其他项目组人员不能查看(没有权限)。
-
在web中什么是权限?
- 一个url就是一个权限。一个url就是一个功能,有没有权限去访问这个功能。权限就是将某个url分配给某个人。
权限表结构设计
第一版
用户表:id | name
权限表:id | url
基于用户设计表结构,每个人的不同权限是url的访问权限。一个人有多个权限,一个权限对应多个人,所以两张表是多对多关系。
用户权限关系表: 用户id | 权限id
- 问题一:当表中已经有一系列的用户(A销售人员,B销售主管,C经理)和其对应的权限(每个用户是个权限),当在增加一个经理人员时,对第三张表的操作?
答:经理C有十条权限,增加经理D时,需要在第三张表中插入十条数据,即经理D和其权限对应表。 - 问题二:给表中某一类人增加权限时,即给所有的销售人员(100个人)增加三条权限,对第三张关系表的操作?
答:需要拿到用户表中每个人的id,再插入3条记录到第三张关系表。需要做100次查询,300次插入,操作比较繁琐。
第一版存在的缺点是:给某一类人增加权限时需要先查找再给每个人增加权限,某一类增加新成员时,需要将这类人的所有权限重新插入,给数据库的压力很大。
第二版
设计思路是,将人进行划分、分类,给人分配角色,基于角色的权限设计。
表结构:
权限表 id | name
权限角色表 权限id | 角色id
角色表 id | 角色
用户角色表 角色 id | 用户id
用户表 id | 用户
表关系:
权限表–角色表 多对多
用户表–角色表 多对多
解决了第一版中的问题。
ORM 模型类代码
from django.db import models
class Permission(models.Model):
"""
权限表
"""
title = models.CharField(verbose_name='标题', max_length=32)
url = models.CharField(verbose_name='含正则的URL', max_length=128)
def __str__(self):
return self.title
class Role(models.Model):
"""
角色
"""
title = models.CharField(verbose_name='角色名称', max_length=32)
permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)
def __str__(self):
return self.title
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)
def __str__(self):
return self.name
基于admin权限信息录入
现有的url:
客户管理
客户列表:/customer/list/
添加客户:/customer/add/
删除客户:/customer/list/(?P<cid>\d+)/
修改客户:/customer/edit/(?P<cid>\d+)/
批量导入:/customer/import/
下载模板:/customer/tpl/
账单管理
账单列表:/payment/list/
添加账单:/payment/add/
删除账单:/payment/del/(?P<pid>\d+)/
修改账单:/payment/edit/<?P<pid>\d+/
在权限组件中录入相关信息:
录入权限
创建用户
创建角色
用户分配角色
角色分配权限
这么一来,用户登录时,就可以根据自己的【用户】找到所有的角色,再根据角色找到所有的权限,再将权限信息放入session,以后每次访问时候需要先去session检查是否有权访问。
录入流程:
- 创建超级用户:
python manage.py createsuperuser
- 在rbac的admin中添加
admin.site.register(models.Permission) admin.site.register(models.UserInfo) admin.site.register(models.Role)
- 访问admin后台管理系统
http://127.0.0.1:8000/admin
录入权限信息。 - 录入人员信息和角色信息进行权限分配。
快速实现简单的权限控制
设计思路
- 登录界面是否有权限访问。
- POST请求,检查用户登录是否合法。
- 用户第一次登陆后获取用户的所有权限放入session中。
- 再次向服务器发起请求,后端编写中间件对当前用户访问的url进行权限判断(是否在session中)。
代码编写
获取用户权限放入session中
重点是对当前用户所有权限获取的思考。
def login(request):
if request.method == "GET":
return render(request, 'login.html')
name = request.POST.get('username')
pwd = request.POST.get('pwd')
current_user = models.UserInfo.objects.filter(name=name, password=pwd).first()
if not current_user:
return render(request, 'login.html', {'msg': '用户名或者密码错误'})
'''
1. 获取用户当前权限
1.1 current_user.roles.all().values("permissions__id", "permissions__id")
此时获取的是有bug的。一个人有多个角色,多个角色有相同的权限时,拿到的权限中有重复的url(三个角色中都有A权限,每个角色中都拿了一次A
权限,相当于获取的内容中有3个A权限对应的url),所以需要去重。
1.2 current_user.roles.all().values("permissions__id", "permissions__url").distinct()
1.3 此时还有一个小问题,当我们给插入一个人角色,没有分配权限时(权限为空),我们取某个用户时,就会出现拿到权限为空的场景,空值不是我们需要
的,这种情况得排除掉。
有一个场景,插入一个'金牌销售'的角色,没有给分配任何权限,在Roles中就会有这样一条记录:
title permissions
'金牌讲师' null
current_user.roles.all().values("permissions__id", "permissions__url").distinct() 拿到的结果中有一条记录:
permissions__id permissions__id
null null
这并符合我们的需求,我们需要拿到的时该用户所有的可用权限,这条记录应该排除掉
current_user.roles.filter(permissions__isnull = False).values("permissions__id", "permissions__url").distinct()
'''
# 当前用户所有权限
permission_queryset = current_user.roles.filter(permissions__isnull=False).values("permissions__id", "permissions__url").distinct()
# 获取权限中的url
permission_list = [item.get('permissions__url') for item in permission_queryset]
print(permission_queryset)
# 放入session中
request.seesion['permission_url_list'] = permission_list
return redirect('/custom/list/')
简易中间件权限校验
在中间件中对所有的请求进行拦截检测,检测当前登录用户访问的url是否在已拥有的权限url中。
重点:mindware中间件流程,自定义中间件,re正则权限匹配,引出白名单的思考,importlib模块的了解。
实现步骤:
# my_md.py
import re
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
class PermisionControl(MiddlewareMixin):
def process_request(self, request):
'''
1. 拿到当前用户请求的url
2. 获取当前用户在session中保存的权限列表
3. 权限信息匹配
'''
current_url = request.path_info
# 有一些权限是所有人默认都有的,不需要做权限判断,先进行一个白名单判断,如果是白名单url,就不用再走权限判断了
valid_url_list = ['/login/'] # 白名单里可以设置,完整字符串,正则(以什么开头,以什么结尾)
for valid_url in valid_url_list:
if re.match(valid_url, current_url):
# 白名单中的url,无需权限验证
# 返回None,继续走后续步骤
return None
permission_list = request.session.get('permission_url_list')
if not permission_list:
# 返回 HttpResponse 不走 后续步骤,直接返回到页面
return HttpResponse('未获取到用户权限信息')
print(current_url)
print(permission_list)
flag = False
for url in permission_list:
regx = '^{}$'.format(url)
if re.match(regx, current_url):
flag = True
break
if not flag:
return HttpResponse('无权访问')
权限控制代码改进(将权限相关的功能放到rbac)
用户登录和权限初始化功能拆分
在视图函数login中,功能可以分为两个部分,用户登录、权限信息初始化。权限信息初始化功能是一个通用的功能,可以将其提取出来,到我们的rbac部分,以后不用写重复代码。
- 新建py文件
2.代码
init_permission.py
def init_permission(current_user, request):
'''
初始化权限信息
:param current_user: 当前登录用户
:param request: 请求相关参数
:return:
'''
# 当前用户所有权限
permission_queryset = current_user.roles.filter(permissions__isnull=False).values("permissions__id", "permissions__url").distinct()
# 获取权限中的url
permission_list = [item.get('permissions__url') for item in permission_queryset]
# 放入session中
request.session['permission_url_list'] = permission_list
account.py
from django.shortcuts import render, redirect, HttpResponse
# 最好不要从其他app中导入模型类,这是不规范的写法
from rbac import models
from rbac.service.init_permission import init_permission
def login(request):
if request.method == "GET":
return render(request, 'login.html')
name = request.POST.get('username')
pwd = request.POST.get('pwd')
current_user = models.UserInfo.objects.filter(name=name, password=pwd).first()
if not current_user:
return render(request, 'login.html', {'msg': '用户名或者密码错误'})
init_permission(current_user, request)
return redirect('/customer/list/')
session中key名字使用的思考
在我们上面的代码中session的key是直接使用字符串写的,我们任何一处使用这个key的地方都需要来init_permission中拷贝一份,这到问题不大,我们复制粘贴就好了。麻烦的是需求某一天变了,key的内容修改称其他值,就要找到项目中所有的key字符串进行替换。所以我们将sessino的key写入到配置文件中,后续修改一个地方就可以了。
在settings.py中写入
PERMISSION_SESSION_KEY = 'permission_url_list'
将使用字符串的地方替换为使用配置文件中的常量
from django.conf import settings
# 替换代码
permission_list = request.session.get(settings.PERMISSION_SESSION_KEY)
request.session[settings.PERMISSION_SESSION_KEY] = permission_list
自定义中间件移到rbac
自定义中间件也是一个通用的功能,我们将其从web app中移植到rbac中。
rbac.py文件
import re
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
from django.conf import settings
class RbacMiddleware(MiddlewareMixin):
def process_request(self, request):
'''
1. 拿到当前用户请求的url
2. 获取当前用户在session中保存的权限列表
3. 权限信息匹配
'''
current_url = request.path_info
# 有一些权限是所有人默认都有的,不需要做权限判断,先进行一个白名单判断,如果是白名单url,就不用再走权限判断了
valid_url_list = ['/login/'] # 白名单里可以设置,完整字符串,正则(以什么开头,以什么结尾)
for valid_url in valid_url_list:
if re.match(valid_url, current_url):
# 白名单中的url,无需权限验证
# 返回None,继续走后续步骤
return None
permission_list = request.session.get(settings.PERMISSION_SESSION_KEY)
if not permission_list:
# 返回 HttpResponse 不走 后续步骤,直接返回到页面
return HttpResponse('为获取到用户权限信息')
print(current_url)
print(permission_list)
flag = False
for url in permission_list:
regx = '^{}$'.format(url)
if re.match(regx, current_url):
flag = True
break
if not flag:
return HttpResponse('无权访问')
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.middlewares.rbac.RbacMiddleware',
]
中间件中白名单的思考
在中间件中需要获取白名单,现在我们是写死的(不能写死在代码中),白名单需要使用到rabc组件的用户来定义,我们需要将其写入到配置文件中,从配置文件中拿白名单。
valid_url_list = settings.VALID_URL_LIST