day103
1-权限系统的实现原理:
创建应用
python manage.py startapp app01
python manage.py startapp rbac
我们的目标:写一个权限系统的组件
rbac:基于角色的权限访问控制(Role-Based Access Control)
在rbac中编辑models.py:
from django.db import models
class Permission(models.Model):
"""
权限表
"""
title = models.CharField(max_length=32)
url = models.CharField(max_length=32)
# 此处url不用URLField因为:
# URLField体现在数据库也是字符串,ModelForm(或Django admin)的时候会用到
class Role(models.Model):
"""
角色表
"""
title = models.CharField(max_length=32)
permissions = models.ManyToManyField(to='Permission')
class UserInfo(models.Model):
"""
用户表
"""
name = models.CharField(max_length=32)
passworld = models.CharField(max_length=64)
email = models.CharField(max_length=32)
roles = models.ManyToManyField(to='Role')
注册app应用
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01.apps.App01Config',
'rbac.apps.RbacConfig',
]
注意:注册app这里'rbac.apps.RbacConfig',不要再写成'rbac',
生成数据库:
python manage.py makemigrations
python manage.py migrate
注意:当前django版本是1.11.8 在之前的版本templates配置与当前的配置方法不同,当前版本配置应该如下:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')]
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
Django不同版本配置文件区别:
10上:
中间件名字:MIDDLEWARE_CLASSES
中间件:执行process_request 的时候有return的时候,7、8、9版本中 走到最后往回走
10下:
templates配置与之前的不一样
中间件名字:MIDDLEWARE
中间件:执行process_request 的时候有return的时候,直接同级往回走
通过admin插入数据(rbac/admin.py)
from django.contrib import admin
from . import models
admin.site.register(models.Permission)
admin.site.register(models.Role)
admin.site.register(models.UserInfo)
创建超级用户
python manage.py createsuperuser
Username : lichengguang
Email address: lcgsmile@qq.com
Password:root!2345
进入admin后编辑 权限发现 字段为Permission object,进入admin.py进行修改(也可以改models.py)
class PermissionConfig(admin.ModelAdmin):
list_display = ['title', 'url']
admin.site.register(models.Permission,PermissionConfig)
再进入后台添加权限:
TITLE URL
修改订单 /orders/edit/(\d+)/
删除订单 /orders/del/(\d+)/
添加订单 /orders/add/
订单列表 /orders/
修改用户 /users/edit/(\d+)/
删除用户 /users/del/(\d+)/
添加用户 /user/add/
用户列表 /users/
添加角色(Role):
发现permissions多选框里全是 Perminssion object,这个是通过Model Form 渲染的,必须通过__str__()修改:
class Permission(models.Model):
"""
权限表
"""
title = models.CharField(max_length=32)
url = models.CharField(max_length=32)
# 此处url不用URLField因为:
# URLField体现在数据库也是字符串,ModelForm(或Django admin)的时候会用到
def __str__(self):
return self.title
class Role(models.Model):
"""
角色表
"""
title = models.CharField(max_length=32)
permissions = models.ManyToManyField(to='Permission')
def __str__(self):
return self.title
class UserInfo(models.Model):
"""
用户表
"""
name = models.CharField(max_length=32)
passworld = models.CharField(max_length=64)
email = models.CharField(max_length=32)
roles = models.ManyToManyField(to='Role')
def __str__(self):
return self.name
再添加角色(Role):
销售:
添加订单 /orders/add/
订单列表 /orders/
销售主管 :
修改订单 /orders/edit/(\d+)/
删除订单 /orders/del/(\d+)/
添加订单 /orders/add/
订单列表 /orders/
用户列表 /users/
老板:
修改订单 /orders/edit/(\d+)/
删除订单 /orders/del/(\d+)/
添加订单 /orders/add/
订单列表 /orders/
修改用户 /users/edit/(\d+)/
删除用户 /users/del/(\d+)/
添加用户 /user/add/
用户列表 /users/
添加用户:
方景红 销售
强哥 老板
力哥 销售主管 销售
龙哥 销售
密码都是123 邮箱都是123@qq.com
此时 ,老板强哥 什么权限都有。力哥既扮演者销售主管有扮演着销售的角色
每个人登陆的时候应显示根据角色不同而显示不同的内容
力哥既是销售又是销售主管,肯定是有重复的权限,要进行去重
销售:
添加订单 /orders/add/
订单列表 /orders/
销售主管 :
修改订单 /orders/edit/(\d+)/
删除订单 /orders/del/(\d+)/
添加订单 /orders/add/
订单列表 /orders/
用户列表 /users/
so,下一步任务:用户登陆获取当前用户所有的权限
登陆界面不归rbac 就在app01里写
第一步:
PermissionComponent/urls.py
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),
]
app01/views.py:
from django.shortcuts import render, HttpResponse
from rbac import models
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, passworld=pwd).first()
if user:
# 登陆成功
# 根据当前用户对象,找到用户关联的所有权限
return HttpResponse('登陆成功')
return render(request, 'login.html')
templates/login.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form method="post">
{% csrf_token %}
<input type="text" name="user">
<input type="password" name="pwd">
<input type="submit" value="提交">
</form>
</body>
</html>
接下来需要思考的是登陆成功后,如何根据当前用户对象,找到用户关联的所有权限(去重)
if user:
# 登陆成功
# 根据当前用户对象,找到用户关联的所有权限
permission_list=user.roles.all().values('title','permissions__title','permissions__url')
for item in permission_list:
print(item)
return HttpResponse('登陆成功')
登陆力哥测试:
{'title': '销售', 'permissions__title': '订单列表', 'permissions__url': '/orders/'}
{'title': '销售', 'permissions__title': '添加订单', 'permissions__url': '/orders/add/'}
{'title': '销售主管', 'permissions__title': '用户列表', 'permissions__url': '/users/'}
{'title': '销售主管', 'permissions__title': '订单列表', 'permissions__url': '/orders/'}
{'title': '销售主管', 'permissions__title': '添加订单', 'permissions__url': '/orders/add/'}
{'title': '销售主管', 'permissions__title': '删除订单', 'permissions__url': '/orders/del/(\\d+)/'}
{'title': '销售主管', 'permissions__title': '修改订单', 'permissions__url': '/orders/edit/(\\d+)/'}
打印结果可以看出 权限筛选出来了,但是还需要对重复的权限进行去重操作
因此,我们可以在筛选的时候就不要角色了,直接把权限筛选出来再进行去重
if user:
# 登陆成功
# 根据当前用户对象,找到用户关联的所有权限
permission_list=user.roles.all().values('permissions__title','permissions__url').distinct()
for item in permission_list:
print(item)
return HttpResponse('登陆成功')
再次登陆力哥测试:
{'permissions__title': '订单列表', 'permissions__url': '/orders/'}
{'permissions__title': '添加订单', 'permissions__url': '/orders/add/'}
{'permissions__title': '用户列表', 'permissions__url': '/users/'}
{'permissions__title': '删除订单', 'permissions__url': '/orders/del/(\\d+)/'}
{'permissions__title': '修改订单', 'permissions__url': '/orders/edit/(\\d+)/'}
去重成功,这样每个人都能拿到权限了,
但是这里还有一个问题,有可能存在角色没有分配权限的情况,修改一下角色表,添加 blank=True
class Role(models.Model):
"""
角色表
"""
title = models.CharField(max_length=32)
permissions = models.ManyToManyField(to='Permission', blank=True)
def __str__(self):
return self.title
创建一个没有权限的临时角色给方景红
这个时候让方景红登陆:
{'permissions__title': '订单列表', 'permissions__url': '/orders/'}
{'permissions__title': '添加订单', 'permissions__url': '/orders/add/'}
{'permissions__title': None, 'permissions__url': None}
可以看到,方景红的权限里的None也被选出来了,所以需要顾虑一下这样的情况all()改成filter(permissions__title__isnull=False)
if user:
# 登陆成功
# 根据当前用户对象,找到用户关联的所有权限
permission_list=user.roles.filter(permissions__title__isnull=False).values('permissions__title','permissions__url').distinct()
for item in permission_list:
print(item)
return HttpResponse('登陆成功')
这个时候再方景红登陆的时候,没有权限就不显示None了
{'permissions__title': '订单列表', 'permissions__url': '/orders/'}
{'permissions__title': '添加订单', 'permissions__url': '/orders/add/'}
其中permission_list的值是一个QuerySet(类似列表):
<QuerySet [{'permissions__title': '订单列表', 'permissions__url': '/orders/'}, {'permissions__title': '添加订单', 'permissions__url': '/orders/add/'}]>
然后再将当前登陆用户的权限(url)放入session中
if user:
# 登陆成功
# 根据当前用户对象,找到用户关联的所有权限
# 将当前登陆的用户的权限写入session
permission_list = user.roles.filter(permissions__title__isnull=False).values('permissions__title',
'permissions__url').distinct()
url_list = []
for item in permission_list:
url_list.append(item['permissions__url'])
request.session['xxxxx'] = url_list
return HttpResponse('登陆成功')
第二步,当用户再访问其他的url的时候就会通过url与session里的url进行比较,判断是否有权限访问这个url.
写一个users函数
urls.py
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),
]
views.py
def users(request):
# 2.获取当前url(通过request.path_info),与session中的url进行比较
url_list = request.session['xxxxx']
for reg in url_list:
print(reg)
return HttpResponse('用户列表')
让方景红登陆测试一下打印结果:
/orders/
/orders/add/
我们再看一下力哥的session里的url:
/orders/
/orders/add/
/users/
/orders/del/(\d+)/
/orders/edit/(\d+)/
可见有人的session里的url中还包含正则表达式,就不可以简单的用等于号或者in进行比较
下面就要用正则re模块语法对当前访问的url与session中的url进行匹配比较。
import re
def users(request):
# 2.获取当前url(通过request.path_info),与session中的url进行比较
url_list = request.session['xxxxx']
flag = False
for reg in url_list:
if re.match(reg, request.path_info):
flag = True
break
if not flag:
return HttpResponse('无权访问')
return HttpResponse('用户列表')
现在,方景红就无权访问users了,而力哥跟强哥是可以访问到的。
用户列表搞完了还得再搞订单列表等....是不是权限判断还得再写一遍?
写到这,我们会发现,这个权限验证每个函数都会用到,因此我们想到了中间件!(就是这么神奇)
2-权限系统代码归类
rbac里创建 中间件 middlewares/rbac.py,把认证的那段代码剪切过来
from django.utils.deprecation import MiddlewareMixin
import re
from django.shortcuts import HttpResponse
class RbacMiddleware(MiddlewareMixin):
# 2.获取当前url(通过request.path_info),与session中的url进行比较
def process_request(self, request):
url_list = request.session['xxxxx']
flag = False
for reg in url_list:
if re.match(reg, request.path_info):
flag = True
break
if not flag:
return HttpResponse('无权访问')
将中间件配置到settigs.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',
]
此时登陆发现,login也无权访问了,所以要把这样的url加入白名单(通过return None),任何人都能访问
from django.utils.deprecation import MiddlewareMixin
import re
from django.shortcuts import HttpResponse
class RbacMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.path_info == '/login/':
return None
url_list = request.session['xxxxx']
flag = False
for reg in url_list:
if re.match(reg, request.path_info):
flag = True
break
if not flag:
return HttpResponse('无权访问')
创建一个用于初始化权限的rbac/service/init_permission.py,把初始化权限的那段代码切过来,在登陆那里只需要调用一下。使登陆逻辑更简单
rbac/service/init_permission.py:
def init_permission(user, request):
"""
获取当前用户权限信息,并放入到session中
"""
permission_list = user.roles.filter(permissions__title__isnull=False).values('permissions__title',
'permissions__url').distinct()
url_list = []
for item in permission_list:
url_list.append(item['permissions__url'])
request.session['xxxxx'] = url_list
app01/views.py:
from django.shortcuts import render, HttpResponse
from rbac import models
import re
from rbac.service.init_permission import init_permission
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, password=pwd).first()
if user:
# 初始化权限信息
init_permission(user,request)
return HttpResponse('登陆成功')
return render(request, 'login.html')
def users(request):
return HttpResponse('用户列表')
def orders(request):
return HttpResponse('订单列表')
3-权限系统的基本实现
将sessions名写配置文件里,哪里用到直接导入就行。
###################权限相关####################
PERMISSION_SESSION_KEY = 'xxxxx'
init_permission.py里调用:
from django.conf import settings # 全局 settings
def init_permission(user, request):
"""
获取当前用户权限信息,并放入到session中
"""
permission_list = user.roles.filter(permissions__title__isnull=False).values('permissions__title',
'permissions__url').distinct()
url_list = []
for item in permission_list:
url_list.append(item['permissions__url'])
request.session[settings.PERMISSION_SESSION_KEY] = url_list
现在又有问题了,白名单里只有一个login吗?admin呢?我们应该自己可以配置白名单
settings.py:
###################权限相关####################
PERMISSION_SESSION_KEY = 'xxxxx'
PERMISSION_VALID_URL = [
'/login/',
'/admin/.*',
]
rbac.py:
from django.utils.deprecation import MiddlewareMixin
import re
from django.shortcuts import HttpResponse
from django.conf import settings
class RbacMiddleware(MiddlewareMixin):
def process_request(self, request):
# 1.获取白名单,让当前url与白名单中的url匹配
for reg in settings.PERMISSION_VALID_URL:
if re.match(reg, request.path_info):
return None
# 获取权限
url_list = request.session.get(settings.PERMISSION_SESSION_KEY)
if not url_list:
return HttpResponse('未获取到当前用户的权限信息,无法访问')
# 对用户访问的url进行匹配
flag = False
for reg in url_list:
if re.match(reg, request.path_info):
flag = True
break
if not flag:
return HttpResponse('无权访问')
4-权限系统之找bug
增加一个添加订单的函数,进入admin后台把销售的添加订单权限去掉
urls.py:
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/add/', views.orders_add),
url(r'^orders/', views.orders),
]
views.py:
def orders_add(request):
return HttpResponse('添加订单')
此时发现作为销售的方景红能够访问http://127.0.0.1:8000/orders/add 添加订单,这是bug呀
问题出现在re模块,我写一个demo试试re模块
import re
v1 = re.match('/orders/', '/orders/')
print(v1) # <_sre.SRE_Match object; span=(0, 8), match='/orders/'>
v2 = re.match('/orders/', '/orders/add')
print(v2) # <_sre.SRE_Match object; span=(0, 8), match='/orders/'>
解决方案,加个起始终止符:
import re
v1 = re.match('/orders/', '/orders/')
print(v1) # <_sre.SRE_Match object; span=(0, 8), match='/orders/'>
v2 = re.match('/orders/', '/orders/add')
print(v2) # <_sre.SRE_Match object; span=(0, 8), match='/orders/'>
v3 = re.match('^/orders/$', '/orders/')
print(v3) # <_sre.SRE_Match object; span=(0, 8), match='/orders/'>
v4 = re.match('^/orders/$', '/orders/add')
print(v4) # None
同理改一下rbac.py:
from django.utils.deprecation import MiddlewareMixin
import re
from django.shortcuts import HttpResponse
from django.conf import settings
class RbacMiddleware(MiddlewareMixin):
def process_request(self, request):
print(request.path_info)
# 1.获取白名单,让当前url与白名单中的url匹配
for reg in settings.PERMISSION_VALID_URL:
if re.match(reg, request.path_info):
return None
# 获取权限
url_list = request.session.get(settings.PERMISSION_SESSION_KEY)
if not url_list:
return HttpResponse('未获取到当前用户的权限信息,无法访问')
# 对用户访问的url进行匹配
flag = False
for reg in url_list:
regx = '^%s$' % (reg,)
if re.match(regx, request.path_info):
flag = True
break
if not flag:
return HttpResponse('无权访问')
urls.py 里面记得下次都加$:
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),
]
5-权限系统之解决粒度精确到按钮级别
models里增加一张权限组表
class PermissionGroup(models.Model):
"""
权限组
"""
caption=models.CharField(max_length=32)
def __str__(self):
return self.caption
生产数据库,创建几个权限组
1 用户组
2 订单组
权限关联权限组
class Permission(models.Model):
"""
权限表
"""
title = models.CharField(max_length=32)
url = models.CharField(max_length=32)
group=models.ForeignKey(to='PermissionGroup',default=1)
# 此处url不用URLField因为:
# URLField体现在数据库也是字符串,ModelForm(或Django admin)的时候会用到
def __str__(self):
return self.title
rbac/admin.py里面把权限组也配置一下
class PermissionConfig(admin.ModelAdmin):
list_display = ['title', 'url','group']
进入admin后台把订单组的权限分配给订单组,通过组区分开来
TITLE URL GROUP
修改订单 /orders/edit/(\d+)/ 订单组
删除订单 /orders/del/(\d+)/ 订单组
添加订单 /orders/add/ 订单组
订单列表 /orders/ 订单组
修改用户 /users/edit/(\d+)/ 用户组
删除用户 /users/del/(\d+)/ 用户组
添加用户 /user/add/ 用户组
用户列表 /users/ 用户组
权限表加个code字段:
class Permission(models.Model):
"""
权限表
"""
title = models.CharField(max_length=32)
url = models.CharField(max_length=32)
code=models.CharField(max_length=16,default='list')
group=models.ForeignKey(to='PermissionGroup',default=1)
# 此处url不用URLField因为:
# URLField体现在数据库也是字符串,ModelForm(或Django admin)的时候会用到
def __str__(self):
return self.title
并在admin.py里配置显示code:
class PermissionConfig(admin.ModelAdmin):
list_display = ['title', 'url','group','code']
code就是一个代号
此时:
TITLE URL GROUP CODE
修改订单 /orders/edit/(\d+)/ 订单组 edit
删除订单 /orders/del/(\d+)/ 订单组 del
添加订单 /orders/add/ 订单组 add
订单列表 /orders/ 订单组 list
修改用户 /users/edit/(\d+)/ 用户组 edit
删除用户 /users/del/(\d+)/ 用户组 del
添加用户 /user/add/ 用户组 add
用户列表 /users/ 用户组 list
rbac:
init_permission.py:
from django.conf import settings
def init_permission(user, request):
"""
获取当前用户权限信息,并放入到session中。
:param user:
:param request:
:return:
"""
permission_list = user.roles.filter(permissions__title__isnull=False).values('permissions__title', # 权限标题
'permissions__group_id', # 权限所在的组ID
'permissions__code', # 权限代号
'permissions__url').distinct()
for item in permission_list:
print(item)
permission_dict = {}
for item in permission_list:
group_id = item['permissions__group_id']
if group_id in permission_dict:
permission_dict[group_id]['urls'].append(item['permissions__url'])
permission_dict[group_id]['codes'].append(item['permissions__code'])
else:
permission_dict[group_id] = {
'urls': [item['permissions__url'], ],
'codes': [item['permissions__code'], ]
}
request.session[settings.PERMISSION_SESSION_KEY] = permission_dict
rbac.py:
from django.utils.deprecation import MiddlewareMixin
import re
from django.shortcuts import HttpResponse
from django.conf import settings
class RbacMiddleware(MiddlewareMixin):
def process_request(self, request):
# 1. 获取白名单,让白名单中的所有url和当前访问url匹配
for reg in settings.PERMISSION_VALID_URL:
if re.match(reg, request.path_info):
return None
# 2. 获取权限
permission_dict = request.session.get(settings.PERMISSION_SESSION_KEY)
if not permission_dict:
return HttpResponse('未获取到当前用户的权限信息,无法访问')
"""
permission_dict={
1:{
'urls': ['/users/','/users/add/','/users/del/(\d+)/','/users/edit/(\d+)/'],
'codes':['list','add','del','edit']
},
2:{
'urls': ['/users/','/users/add/',],
'codes':['list','add',]
},
}
"""
flag = False
# 3. 对用户请求的url进行匹配
for value in permission_dict.values():
urls = value['urls']
codes = value['codes']
for reg in urls:
regx = "^%s$" % (reg,)
if re.match(regx, request.path_info):
flag = True
break
if flag:
request.permission_codes = codes
break
if not flag:
return HttpResponse('无权访问')
app01/views.py:
def users(request):
print(request.permission_codes)
return render(request, 'users.html')
users.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>用户列表</h1>
{% if 'add' in request.permission_codes %}
<a>添加</a>
{% endif %}
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>六五</td>
<td>
{% if 'edit' in request.permission_codes %}
<a>编辑</a>
{% endif %}
{% if 'del' in request.permission_codes %}
<a>删除</a>
{% endif %}
</td>
</tr>
</tbody>
</table>
</body>
</html>
此时就达到粒度按钮级别,下面一个问题是菜单的显示:
菜单这里,用户列表,添加用户显示,编辑和删除因为涉及了编辑删除谁,所以点进列表后才编辑和删除,所以不显示在菜单里,