权限组件开发
为什么是什么?
1.为什么要开发关于权限相关的系统?
在现实生活中,就有很多的关于权限的场景,比如:在超市,店长的权限永远比员工权限大。在公司,老板的权限永远比经理大。老板可以知道全部员工的工资多少,而经理只能知道自己部门的工资。这些就是权限的体现。
2.权限是什么?
在web系统中一个url就是一个权限,那么一个人能访问的url,那么就有多少个权限
权限的表结构
第一版权限结构
用户表
ID
User
1
小明
2
小张
3
小李
用户与权限(多对多表)
ID
user_id(与user表形成1对多关系)
Permissions_id(与权限表形成1对多关系)
1
1
1
1
2
1
1
3
1
1
1
2
权限表(url表)
ID
Url
Shows
1
/user/index
用户首页
2
/home
网站首页
3
/user/del
用户删除
实现: '用户' 直接对应 '权限' ,通过'第三张表的多对多关系' 绑定对应的权限关系
当前实现了对权限的关联,但是存在弊端!
缺点与弊端:
1. 如果有新来的用户需要权限,需要在 关联表中 进行1 条1 条的设置
2. 如果对某些用户需要修改权限,那么需要找出这类用户在 关联表中 进行增删改权限
3. 如果权限表中得权限是大量的(1000 个),用户的体量也很大(50000 个) 那么每一次修改需要花费大量时间
第二版权限结构
用户表
ID
User
1
小明
2
小张
3
小李
4
小王
用户表 and 角色表(多对多关系)
ID
user_id
role_id
1
1
1(人事经理权限)
2
2
2(部门经理权限)
3
3
2(部门经理权限)
4
4
3(普通员工权限)
角色表
ID
role_name
1
人事经理
2
部门经理
3
员工
角色 and 权限(多对多关系)
ID
role_id
permissions_id
1
1
1,2,3,4,5(角色:人事经理有5条权限)
2
2
1,2,5(角色:部门经理有3条权限)
3
3
1,2(角色:员工有2条权限)
权限表(url表)
ID
Url
Shows
1
/user/index
用户首页
2
/home
网站首页
3
/user/del
用户删除
4
/user/update
用户修改
5
/user/insert
用户添加
5
/user/select
用户查询
RBAC:基于角色控制权限管理
优点:
1.可以通过角色设置权限(1个角色多个权限)
2.如果用户可以拥有多个角色(有多少角色就有多少权限)
3.如果新用户添加,那么直接添加对应的角色即可
4.如果需要修改部分用户的权限,可以创建新的角色绑定权限赋予用户,修改这部分用户的角色权限。
5.可以通过控制角色权限,而控制用户拥有的权限方便管理
在Django中进行创建表结构
from django.db import models
__all__ = ['Permissions' , 'Role' , 'User' ]
class Permissions (models.Model):
class Mate :
db_table = 'Permissions'
id = models.AutoField(primary_key=True )
title = models.CharField(max_length=128 ,verbose_name='标题' )
url = models.CharField(max_length=255 , verbose_name='正则模式下的url' )
def __str__ (self ):
return self.title
class Role (models.Model):
class Mate :
db_table = 'Role'
id = models.AutoField(primary_key=True )
role_name = models.CharField(max_length=255 , verbose_name='角色名称' )
permissions = models.ManyToManyField(to='Permissions' , db_table='Role_Permissions' , verbose_name='对多对权限角色报绑定字段' )
def __str__ (self ):
return self.role_name
class User (models.Model):
class Mate :
db_table = 'User'
id = models.AutoField(primary_key=True )
user_name = models.CharField(max_length=128 , verbose_name='账户' )
password = models.CharField(max_length=128 , verbose_name='密码' )
user_email = models.CharField(max_length=128 , verbose_name='邮箱' )
role = models.ManyToManyField(to='Role' , db_table='User_Role' , verbose_name='多对多用户与角色绑定字段' )
def __str__ (self ):
return self.user_name
基本权限控制
1. 用户登录页面需不需要进行权限控制?
2. 用户在首页时需不需要进行权限控制?
大致的执行流程
1. 用户登录[判断用户的密码和账户,同时从数据库中获取权限的信息]
2. 将权限的信息存储到session或者redis中[注意细节使用redis存储]
1. 如果存储到session中,用户同时登录,但是用户的权限发生变化怎么办?没办法进行实时的控制权限。
2. 建议存储到redis中或者mongodb中,利用一个表示 + 用户的id 作为key进行存储。user_id:[权限1 ,权限2 ,]
3. 当进行对角色权限增删改查时,需要清空redis或者mongodb中对应拥有用户的存储的权限 键值对
4. 当每次登录,或者访问时,需要在api接口或者视图中,判断一下redis中是否存在权限,如果没有,从mysql中取权限存到redis中
3. 当每次请求时[白名单除外(登录url/首页url/..)],从redis或者session中进行判断当前是否存在访问的权限,如果没有返回'报错信息' 如果有 就接着执行下面的中间
权限开发需要功能:
1. 登录[获取权限,存储到session中 or redis or mongodb]
2. 设置一个访问中间件[每次访问,都会将访问的url与权限列表(从存储的地方进行获取)中得url进行对比]
3. 如果通过,接着执行并返回请求数据。没有通过返回 中间件直接返回无权内容[django中间件特性,如果存在返回值就会直接返回内容,不会再进行执行]
快速开发流程
1 .登录页面是否有权限(有)
2 .POST请求,校验用户登录是否合法
3 .获取登录当前用户的相关信息,存储到session中
4 .用户在此访问服务器发起请求: http://xxx .xx.xx/index 后端编写中间件对用户进行访问权限的判断[当前访问的url是否存在权限列表中]
1.用户登录权限获取逻辑
from django.shortcuts import HttpResponse, render, redirect
from rbac.models import User
def login (request ):
......
1. 根据用户获取用户全部角色
获取用户全部的角色对象
user_obj.role.all ()
2. 获取用户权限角色下的全部url
user_obj.role.filter (permissions__isnull=False ).values('permissions__url' ).distinct()
permissions_list = user_obj.role.filter (permissions__isnull=False ).values('permissions__url' ).distinct()
permissions_url_list = [ i.get('permissions__url' ) for i in permissions_list]
print (permissions_url_list)
.....
注意:
1. 在获取角色下的url权限是,需要进行去重
原因:'客户经理' 可以有 index/权限 '部门经理' 也可以 index/权限,如果角色同时拥有这两个角色,权限重复。
user_obj.role.filter (permissions__isnull=False ).values('permissions__url' ).distinct()
select DISTINCT rbac_permissions.url from rbac_user
INNER JOIN User_Role on rbac_user.id = User_Role.user_id
INNER JOIN rbac_role on rbac_role.id = User_Role.role_id
inner join Role_Permissions on rbac_role.id = Role_Permissions.role_id
INNER JOIN rbac_permissions on Role_Permissions.permissions_id = rbac_permissions.id where rbac_user.id = 1
2. 因为权限只是一些url,假设创建的角色关联的权限为空,那么需要进行去空操作
role角色 与 permissions权限 关联关系不能为空,必须有值存在(避免角色关联权限不能为空)
user_obj.role.filter (permissions__isnull=False ).values('permissions__url' ).distinct()
select url from (select DISTINCT rbac_permissions.url from rbac_user
INNER JOIN User_Role on rbac_user.id = User_Role.user_id
INNER JOIN rbac_role on rbac_role.id = User_Role.role_id
inner join Role_Permissions on rbac_role.id = Role_Permissions.role_id
INNER JOIN rbac_permissions on Role_Permissions.permissions_id = rbac_permissions.id where rbac_user.id = 1 ) as permissions_url WHERE permissions_url.url not NULL
2.项目中间件逻辑部分
当用户请求时,进行触发
1. 获取当前用户请求的url地址
2. 获取当前用户在session/reids中保存的权限列表
3. 匹配权限信息
4. 面对大家够可以访问的url权限 需要设置一个白名单进行处理
1. 中间件代码部分:
import re
from django.http import JsonResponse
from django.utils.deprecation import MiddlewareMixin
class RbacMiddlewareMixin (MiddlewareMixin ):
'''
权限中间件认证
process_request请求来时是进行执行
django process_request 特点:存在返回值不在执行其他的中间件与视图,直接返回结果页面(默认返回none)
'''
def process_request (self, request ):
valid_url_list = [
'login/' ,
'admin/.*'
]
current_url = request.path_info.replace('/' ,'' ,1 )
1. 通过正则的方式循环白名单,过滤掉不需要拦截的url
for i in valid_url_list:
if re.match(i,current_url):
return None
2. 从session中获取权限列表
permissions_url_list = request.session.get('permissions_url' )
if not permissions_url_list:
return JsonResponse({"code" :403 ,'error' :'当前用户未登录,请进行登录后再访问' })
flag = False
3. 循环权限列表,使用正则匹配确认当前的权限是否通过
for url in permissions_url_list:
为什么使用正则,正则匹配的更为准确,并且因为url中存在正则的url。
比如: customer/edit/(?P<cid>\d+)/ 使用常规的匹配方式 customer/edit/1 判断无法通过
正则匹配 re.match('customer/edit/(?P<cid>\d+)/' ,'customer/edit/123456/' )
问题:但是使用正则匹配问题:匹配的内容没有终止起始
比如: re.match('customer/edit/' ,'customer/edit/123456/' ) 也开始可以进行匹配到的
解决: 加上正则终止和起始符号
mathc_url = f"^{url} $"
if re.match(mathc_url,current_url):
flag = True
if not flag:
return JsonResponse({"code" :403 ,'error' :'当前用户没有权限,请让管理员添加权限!' })
2. 在配置中将中间件进行注册
MIDDLEWARE = [
'......process_request'
]
3.中间件逻辑部分-优化
1. 将使用的变量放到配置文件夹中
config.py
permissions_key = 'permissions_url'
valid_url_list = [
'login/' ,
'admin/.*'
]
2. 将登录功能对权限的初始化分解(登录 与 权限初始化)
登录功能只需要导入当前函数进行初始化即可
permission_init.py
from rbac.config import permissions_key
def init_permission (user_obj,request ):
'''
在登录时导入,进行权限的初始化(当前用户权限记录)
user_obj: 用户对象,通过orm 获取用户的权限列表
request: 请求对象,将权限存放在session中
'''
permissions_list = user_obj.role.filter (permissions__isnull=False ).values('permissions__url' ).distinct()
permissions_url_list = [ i.get('permissions__url' ) for i in permissions_list]
request.session[permissions_key] = permissions_url_list
3. 中间件进行微调
import re
from django.http import JsonResponse
from django.utils.deprecation import MiddlewareMixin
from rbac.config import permissions_key,valid_url_list
class RbacMiddlewareMixin (MiddlewareMixin ):
'''权限中间件认证'''
def process_request (self, request ):
'''
当用户请求时,进行触发
1.获取当前用户请求的url地址
2.获取当前用户在session中保存的权限列表
3.匹配权限信息
'''
current_url = request.path_info.replace('/' ,'' ,1 )
for i in valid_url_list:
if re.match(i,current_url):
return None
permissions_url_list = request.session.get(permissions_key)
if not permissions_url_list:
return JsonResponse({"code" :403 ,'error' :'当前用户未登录,请进行登录后再访问' })
flag = False
for url in permissions_url_list:
mathc_url = f"^{url} $"
if re.match(mathc_url,current_url):
flag = True
if not flag:
return JsonResponse({"code" :403 ,'error' :'当前用户没有权限,请让管理员添加权限!' })
权限之菜单
一级菜单设计思路/代码
权限表与菜单的关系 (包含关系)
1. 需要在权限表中设置一个字段,用来表示 当前权限是否可以作为功能
2. 在admin中进行配置当前字段
3. 在权限出初始化函数中将,可以作为菜单的权限存储到列表保存到session中
1. 表结构
from django.db import models
__all__ = ['Permissions' , 'Role' , 'User' ]
class Permissions (models.Model):
class Mate :
db_table = 'Permissions'
id = models.AutoField(primary_key=True )
title = models.CharField(max_length=128 , verbose_name='标题' )
url = models.CharField(max_length=255 , verbose_name='正则模式下的url' )
menu = models.BooleanField(blank=True ,null=True ,verbose_name='当前权限是否可以作为菜单' )
def __str__ (self ):
return self.title
class Role (models.Model):
class Mate :
db_table = 'Role'
id = models.AutoField(primary_key=True )
role_name = models.CharField(max_length=255 , verbose_name='角色名称' )
permissions = models.ManyToManyField(to='Permissions' , db_table='Role_Permissions' , verbose_name='对多对权限角色报绑定字段' ,blank=True )
def __str__ (self ):
return self.role_name
class User (models.Model):
class Mate :
db_table = 'User'
id = models.AutoField(primary_key=True )
user_name = models.CharField(max_length=128 , verbose_name='账户' )
password = models.CharField(max_length=128 , verbose_name='密码' )
user_email = models.CharField(max_length=128 , verbose_name='邮箱' )
role = models.ManyToManyField(to='Role' , db_table='User_Role' , verbose_name='多对多用户与角色绑定字段' )
def __str__ (self ):
return self.user_name
2. 在admin中进行设置对权限进行设置,那些权限可以作为菜单
3. 在权限初始化函数中进行初始化菜单设置
from rbac.config import permissions_key, menu_list
def init_permission (user_obj, request ):
'''
在登录时导入,进行权限的初始化(当前用户权限记录)
user_obj: 用户对象,通过orm 获取用户的权限列表
request: 请求对象,将权限存放在session中
'''
permissions_list = user_obj.role.filter (permissions__isnull=False ).values('permissions__url' ,
'permissions__menu' ,
'permissions__title' ).distinct()
permissions_url_list = [i.get('permissions__url' ) for i in permissions_list]
menu_url_list = [{'menu_url' : i.get('permissions__url' ), 'menu_title' : i.get('permissions__title' )} for i in
permissions_list if i.get('permissions__menu' )]
print (menu_url_list)
request.session[menu_list] = menu_url_list
request.session[permissions_key] = permissions_url_list
仅限前后端不分离使用
在用户登录后,需要将菜单信息给到用户页面中 进行展示
在html页面进行循环展示
<div class ="static-menu" >
{% for menu in request.session.menu_list %}
<a href="/{{ menu.menu_url }}" class ="active" >{{ menu.menu_title }}</a>
{% endfor %}
</div>
这种方式不行,因为需要手动的配置,可以设置一个变量使用的是django inclusion_tag
1. 创建一个文件 rbac/templatetags/menu.py
import os
from django.template import Library
path = os.path.abspath(os.path.dirname(__file__))
register = Library()
@register.inclusion_tag(os.path.join(path, '../templates' , 'static_menu.html' ) )
def static_menu (request ):
'''
1菜单
request 使用当前inclusion_tag方法传入的位置参数
'''
menu_list = request.session['menu_list' ]
for i in menu_list:
if i.get('menu_url' ) == request.path.replace('/' ,'' ,1 ):
i['class' ] = 'active'
return {'menu_list' :menu_list}
2. 创建一个模板 rbac/templates/static_menu.html
接受static_menu方法返回的结果变量
<div class ="static-menu" >
{% for menu in menu_list %}
<a href="/{{ menu.menu_url }}" class ={{ menu.class }} >{{ menu.menu_title }}</a>
{% endfor %}
</div>
3. 使用当前方法的模板
只要其他模板使用就会将 inclusion_tag(传入模板的路径) 将路径的模板进行渲染到使用者的页面中
{% load menu %}
{% static_menu request %}
1. 使用inclusion_tag(模板路径)装饰一个函数
2. 装饰的函数的返回值会给到模板路径中得模板作为参数使用
3. 使用者需要在使用的模板进行 load 当前 inclusion_tag 使用的函数文件名
4. 在需要使用的位置将函数名进行 {% 函数名 参数1 参数2 %}
二级菜单设计思路/代码
1.1 级菜单没有必要进行跳转,只是为了展开二级菜单(可以写死)
2.2 级菜单需要进行跳转,需要url,需要进行跳转
菜单的数据结构:
[
{
title:'1级菜单名称' ,
children:[
{title:'2级菜单1' },
{title:'2级菜单2' },
]
}
]
可以进行双层循环展示
思路
1. 表结构修改
2. session中存储的菜单信息结构进行变化
3. 修改权限初始化的菜单内容部分的信息
4. 需要修改页面显示效果
表结构修改
from django.db import models
__all__ = ['Permissions' , 'Role' , 'User' , 'Menu' ]
class Menu (models.Model):
'''存放1级菜单'''
class Mate :
db_table = 'Menu'
id = models.AutoField(primary_key=True )
title = models.CharField(max_length=128 , verbose_name='1级菜单名称' )
def __str__ (self ):
return self.title
class Permissions (models.Model):
'''权限表'''
class Mate :
db_table = 'Permissions'
id = models.AutoField(primary_key=True )
title = models.CharField(max_length=128 , verbose_name='标题' )
url = models.CharField(max_length=255 , verbose_name='正则模式下的url' )
menu = models.ForeignKey(blank=True , null=True , verbose_name='所属菜单' , to='Menu' , on_delete=models.CASCADE)
def __str__ (self ):
return self.title
class Role (models.Model):
'''角色表'''
class Mate :
db_table = 'Role'
id = models.AutoField(primary_key=True )
role_name = models.CharField(max_length=255 , verbose_name='角色名称' )
permissions = models.ManyToManyField(to='Permissions' , db_table='Role_Permissions' , verbose_name='对多对权限角色报绑定字段' ,
blank=True )
def __str__ (self ):
return self.role_name
class User (models.Model):
'''用户表'''
class Mate :
db_table = 'User'
id = models.AutoField(primary_key=True )
user_name = models.CharField(max_length=128 , verbose_name='账户' )
password = models.CharField(max_length=128 , verbose_name='密码' )
user_email = models.CharField(max_length=128 , verbose_name='邮箱' )
role = models.ManyToManyField(to='Role' , db_table='User_Role' , verbose_name='多对多用户与角色绑定字段' )
def __str__ (self ):
return self.user_name
用户登录权限初始化函数
from rbac.config import permissions_key, menu_session_name
def init_permission (user_obj, request ):
'''
在登录时导入,进行权限的初始化(当前用户权限记录)
user_obj: 用户对象,通过orm 获取用户的权限列表
request: 请求对象,将权限存放在session中
'''
permissions_list = user_obj.role.filter (permissions__isnull=False ).values(
'permissions__url' ,
'permissions__title' ,
'permissions__menu__id' ,
'permissions__menu__title'
).distinct()
permissions_url_list = [i.get('permissions__url' ) for i in permissions_list]
menu_dict = {}
for item in permissions_list:
children_menu = {'permissions_url' : item.get('permissions__url' ),
"permissions_title" : item.get('permissions__title' )}
one_menu_id = item.get('permissions__menu__id' )
if not one_menu_id:
continue
if one_menu_id in menu_dict:
menu_dict.get(one_menu_id).get('children' ).append(children_menu)
else :
menu_dict[one_menu_id] = {
"permissions__menu__title" : item.get('permissions__menu__title' ),
'children' : [children_menu, ]
}
request.session[menu_session_name] = menu_dict
request.session[permissions_key] = permissions_url_list
menu_dict变量构建:
{
1 : {
'permissions__menu__title' : '客户管理' ,
'children' : [
{'permissions_url' : 'customer/list/' , 'permissions_title' : '客户列表' },
{'permissions_url' : 'payment/list/' , 'permissions_title' : '付款列表' }
]
},
2 : {
'permissions__menu__title' : '账单管理' ,
'children' : [
{'permissions_url' : 'customer/list/' , 'permissions_title' : '客户列表' },
{'permissions_url' : 'payment/list/' , 'permissions_title' : '付款列表' }
]
}
....
....
....
}
显示2级菜单(不分离有用)
@register.inclusion_tag(os.path.join(path, '../templates' , 'multi_menu.html' ) )
def menu_menu (request ):
'''
2菜单模板
'''
menu_list = request.session[menu_session_name]
for item in menu_list.values():
for children in item.get('children' ):
if children.get('permissions_url' ) == request.path.replace('/' , '' , 1 ):
children['class' ] = 'active'
item['body_hidden' ] = ''
else :
item['body_hidden' ] = 'hidden'
return {'menu_list' : menu_list}
<div class ="multi-menu" >
{% for menu in menu_list.values %}
<div class ="item" >
<div class ="title" >
{{ menu.permissions__menu__title }}
</div>
<div class ="body {{ menu.body_hidden }}" >
{% for children in menu.children %}
<a href="/{{ children.permissions_url }}"
class ="{{ children.class }}" >{{ children.permissions_title }}</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
不能作为二级菜单的权限与二级菜单做归属关系(不分离有用)
还有好多方式能解决当前的问题
当点击非2 级菜单是,归属的二级菜单直接进行默认选中效果
1. 在登录是重置权初始化
权限的结构要变为:
permissions_key = [
{id :1 ,url:'xxxx/xx/' ,pid:null},
{id :2 ,url:'xxxx/xx/' ,pid:1 },
{id :3 ,url:'xxxx/xx/' ,pid:1 },
]
菜单的结构变为:
menu_session_name = {
1 : {
'permissions__menu__title' : '客户管理' ,
'children' : [
{id :1 ,'permissions_url' : 'customer/list/' , 'permissions_title' : '客户列表' ,pid:null},
{id :2 ,'permissions_url' : 'payment/list/' , 'permissions_title' : '付款列表' ,pid:null}
]
},
}
2. 当用户登录后,再次访问通过中间件时(跳转到首页时的操作)
for i in permissions_key:
当前访问的url归属于那个二级菜单的id = item['pid' ] or item['id' ]
3. 在菜单显示的inclusion_tag的部分,修改判断的条件,变为根据id 判断
for item in menu_list.values():
for children in item.get('children' ):
if children.get('id' ) == 当前访问的url归属于那个二级菜单的id :
children['class' ] = 'active'
item['body_hidden' ] = ''
else :
item['body_hidden' ] = 'hidden'
return {'menu_list' : menu_list}
表结构
from django.db import models
__all__ = ['Permissions' , 'Role' , 'User' , 'Menu' ]
class Menu (models.Model):
'''存放1级菜单'''
class Mate :
db_table = 'Menu'
id = models.AutoField(primary_key=True )
title = models.CharField(max_length=128 , verbose_name='1级菜单名称' )
def __str__ (self ):
return self.title
class Permissions (models.Model):
class Mate :
db_table = 'Permissions'
id = models.AutoField(primary_key=True )
title = models.CharField(max_length=128 , verbose_name='标题' )
url = models.CharField(max_length=255 , verbose_name='正则模式下的url' )
menu = models.ForeignKey(blank=True , null=True , verbose_name='所属菜单' , to='Menu' , on_delete=models.CASCADE)
pid = models.ForeignKey(to='Permissions' , verbose_name='不能成为2级菜单的权限关联到可以成为2级菜单下面 归属关系' , related_name='parents' ,on_delete=models.SET_NULL, null=True , blank=True )
def __str__ (self ):
return self.title
class Role (models.Model):
class Mate :
db_table = 'Role'
id = models.AutoField(primary_key=True )
role_name = models.CharField(max_length=255 , verbose_name='角色名称' )
permissions = models.ManyToManyField(to='Permissions' , db_table='Role_Permissions' , verbose_name='对多对权限角色报绑定字段' ,
blank=True )
def __str__ (self ):
return self.role_name
class User (models.Model):
class Mate :
db_table = 'User'
id = models.AutoField(primary_key=True )
user_name = models.CharField(max_length=128 , verbose_name='账户' )
password = models.CharField(max_length=128 , verbose_name='密码' )
user_email = models.CharField(max_length=128 , verbose_name='邮箱' )
role = models.ManyToManyField(to='Role' , db_table='User_Role' , verbose_name='多对多用户与角色绑定字段' )
def __str__ (self ):
return self.user_name
用户登录权限初始化部分
from rbac.config import permissions_key, menu_session_name
def init_permission (user_obj, request ):
'''
在登录时导入函数,进行权限的初始化(当前用户权限记录)
user_obj: 用户对象,通过orm 获取用户的权限列表
request: 请求对象,将权限存放在session中
'''
permissions_list = user_obj.role.filter (permissions__isnull=False ).values(
"permissions__id" ,
'permissions__url' ,
'permissions__title' ,
'permissions__menu__id' ,
'permissions__menu__title' ,
"permissions__pid__id"
).distinct()
permissions_url_list = []
menu_dict = {}
for item in permissions_list:
1. 权限的全部有用的参数组成一个字典
children_menu = {
"id" : item.get('permissions__id' ),
'permissions_url' : item.get('permissions__url' ),
"permissions_title" : item.get('permissions__title' ),
"pid" :item.get('permissions__pid__id' )
}
2. 将权限添加到权限列表中
permissions_url_list.append(children_menu)
3. 构建1 级菜单与2 级菜单的数据结构
one_menu_id = item.get('permissions__menu__id' )
'''
-- 具体逻辑
3.1.如果没有menu_id 直接跳过(不能成为2级菜单)
3.2.如果当前的menu_id 存在构建的结构中 就将二级菜单进行append添加children中
3.3.如果当前的menu_id 不在 menu_dict 结构中就进行构建
'''
if not one_menu_id:
continue
if one_menu_id in menu_dict:
menu_dict.get(one_menu_id).get('children' ).append(children_menu)
else :
menu_dict[one_menu_id] = {
"permissions_menu_title" : item.get('permissions__menu__title' ),
'children' : [children_menu, ]
}
request.session[menu_session_name] = menu_dict
request.session[permissions_key] = permissions_url_list
中间件修改为
from django.utils.deprecation import MiddlewareMixin
import re
from django.http import JsonResponse
from rbac.config import permissions_key, valid_url_list
class RbacMiddlewareMixin (MiddlewareMixin ):
'''权限中间件认证功能(django 中间件)'''
def process_request (self, request ):
'''
当用户请求时,进行触发
1.获取当前用户请求的url地址
2.获取当前用户在session中保存的权限列表
3.匹配权限信息
'''
1. 当前用户的url 获取的是/customer/list / 进行替换为 customer/list /方便对比
current_url = request.path_info.replace('/' , '' , 1 )
for i in valid_url_list:
if re.match(i, current_url):
return None
permissions_url_list = request.session.get(permissions_key)
if not permissions_url_list:
return JsonResponse({"code" : 403 , 'error' : '当前用户未登录,请进行登录后再访问' })
2. 通过变量控制是否有权限
flag = False
3. 循环从session获取权限信息
for url in permissions_url_list:
'''
3.1.因为在url中存在 customer/edit/(?P<cid>\d+)/ 正则的url需要使用正则进行匹配
例如:customer/edit/1 正则匹配 customer/edit/(?P<cid>\d+)/
3.2.通过有有权限 但是使用正则匹配问题:匹配的内容没有终止起始
例如: re.match('customer/edit/','customer/edit/123456/') 也开始可以进行匹配到的
3.3.解决: 加上正则终止和起始符号
3.4. 有权flag限赋值为True,如果是False那么就没有权限
'''
re_url = f"^{url.get('permissions_url' )} $"
if re.match(re_url, current_url):
request.menu_id = url.get('pid' ) or url.get('id' )
flag = True
if not flag:
return JsonResponse({"code" : 403 , 'error' : '当前用户没有权限,请让管理员添加权限!' })
二级菜单inclusion_tag修改
@register.inclusion_tag(os.path.join(path, '../templates' , 'multi_menu.html' ) )
def menu_menu (request ):
'''
2菜单模板
'''
menu_list = request.session[menu_session_name]
for item in menu_list.values():
for children in item.get('children' ):
if children.get('id' ) == request.menu_id:
children['class' ] = 'active'
item['body_hidden' ] = ''
else :
item['body_hidden' ] = 'hidden'
print (request.path.replace('/' , '' , 1 ))
return {'menu_list' : menu_list}
multi_menu模板
<div class ="multi-menu" >
{% for menu in menu_list.values %}
<div class ="item" >
<div class ="title" >
{{ menu.permissions_menu_title }}
</div >
<div class ="body {{ menu.body_hidden }}" >
{% for children in menu.children %}
<a href ="/{{ children.permissions_url }}"
class ="{{ children.class }}" >{{ children.permissions_title }}</a >
{% endfor %}
</div >
</div >
{% endfor %}
</div >
权限之导航路由
当进行访问时,需要修改导航的路由内容
因为在二级菜单中得pid已经作何了这层关系,只需要进行添加判断语句
用户登录权限初始化
from rbac.config import permissions_key, menu_session_name
def init_permission (user_obj, request ):
'''
在登录时导入函数,进行权限的初始化(当前用户权限记录)
user_obj: 用户对象,通过orm 获取用户的权限列表
request: 请求对象,将权限存放在session中
'''
permissions_list = user_obj.role.filter (permissions__isnull=False ).values(
"permissions__id" ,
'permissions__url' ,
'permissions__title' ,
'permissions__menu__id' ,
'permissions__menu__title' ,
"permissions__pid__id" ,
"permissions__pid__title" ,
"permissions__pid__url" ,
).distinct()
permissions_url_list = []
menu_dict = {}
for item in permissions_list:
1. 权限的全部有用的参数组成一个字典
children_menu = {
"id" : item.get('permissions__id' ),
'permissions_url' : item.get('permissions__url' ),
"permissions_title" : item.get('permissions__title' ),
"pid" : item.get('permissions__pid__id' ),
"ptitle" : item.get('permissions__pid__title' ),
"purl" : item.get('permissions__pid__url' )
}
2. 将权限添加到权限列表中
permissions_url_list.append(children_menu)
3. 构建1 级菜单与2 级菜单的数据结构
one_menu_id = item.get('permissions__menu__id' )
'''
-- 具体逻辑
3.1.如果没有menu_id 直接跳过(不能成为2级菜单)
3.2.如果当前的menu_id 存在构建的结构中 就将二级菜单进行append添加children中
3.3.如果当前的menu_id 不在 menu_dict 结构中就进行构建
'''
if not one_menu_id:
continue
if one_menu_id in menu_dict:
menu_dict.get(one_menu_id).get('children' ).append(children_menu)
else :
menu_dict[one_menu_id] = {
"permissions_menu_title" : item.get('permissions__menu__title' ),
'children' : [children_menu, ]
}
request.session[menu_session_name] = menu_dict
request.session[permissions_key] = permissions_url_list
中间件修改为
from django.utils.deprecation import MiddlewareMixin
import re
from django.http import JsonResponse
from rbac.config import permissions_key, valid_url_list
class RbacMiddlewareMixin (MiddlewareMixin ):
'''权限中间件认证功能(django 中间件)'''
def process_request (self, request ):
'''
当用户请求时,进行触发
1.获取当前用户请求的url地址
2.获取当前用户在session中保存的权限列表
3.匹配权限信息
'''
1. 当前用户的url 获取的是/customer/list / 进行替换为 customer/list /方便对比
current_url = request.path_info.replace('/' , '' , 1 )
for i in valid_url_list:
if re.match(i, current_url):
return None
permissions_url_list = request.session.get(permissions_key)
if not permissions_url_list:
return JsonResponse({"code" : 403 , 'error' : '当前用户未登录,请进行登录后再访问' })
2. 通过变量控制是否有权限
flag = False
navigation_list = [
{"title" : "首页" , "url" : "#" }
]
3. 循环从session获取权限信息
for url in permissions_url_list:
'''
3.1.因为在url中存在 customer/edit/(?P<cid>\d+)/ 正则的url需要使用正则进行匹配
例如:customer/edit/1 正则匹配 customer/edit/(?P<cid>\d+)/
3.2.通过有有权限 但是使用正则匹配问题:匹配的内容没有终止起始
例如: re.match('customer/edit/','customer/edit/123456/') 也开始可以进行匹配到的
3.3.解决: 加上正则终止和起始符号
3.4. 有权flag限赋值为True,如果是False那么就没有权限
'''
re_url = f"^{url.get('permissions_url' )} $"
if re.match(re_url, current_url):
request.menu_id = url.get('pid' ) or url.get('id' )
flag = True
if url.get('pid' ):
navigation_list.extend(
[{"title" : url.get('ptitle' ), "url" : url.get('purl' )},
{"title" : url.get('permissions_title' ), "url" : url.get('permissions_url' )}]
)
else :
navigation_list.extend([
{"title" : url.get('permissions_title' ), "url" : url.get('permissions_url' )}
])
request.navigation_list = navigation_list
if not flag:
return JsonResponse({"code" : 403 , 'error' : '当前用户没有权限,请让管理员添加权限!' })
添加模板
@register.inclusion_tag(os.path.join(path, '../templates' , 'navigation.html' ) )
def navigation (request ):
'''
导航栏模板
'''
navigation_list = request.navigation_list
return {'navigation_list' : navigation_list}
navigation.html
{% for navigation in navigation_list %}
<li><a href="/{{ navigation.url }}" >{{ navigation.title }}</a></li>
{% endfor %}
{% load menu %}
{% navigation request %}
权限的控制力度到按钮级别
也就是在页面按钮(每一个按钮都是一个发送数据到后端,相当于一个url)都属于权限,也就是将当前这些按钮进行控制。如果没有权限的按钮就进行隐藏或者变为不可点击,如果有权限就进行显示可点击
在前后端不分离处理时模板处理,
根据模板语法进行判断当前是否存在权限
{% if user/add in 权限列表 %}
<a href="user/add" >添加用户</a>
{%endfor%}
因为这种方式太过于繁琐,可以在数据库中添加一个字段 为权限起一个别名的字段
数据库修改
from django.db import models
__all__ = ['Permissions' , 'Role' , 'User' , 'Menu' ]
class Menu (models.Model):
'''存放1级菜单'''
class Mate :
db_table = 'Menu'
id = models.AutoField(primary_key=True )
title = models.CharField(max_length=128 , verbose_name='1级菜单名称' )
def __str__ (self ):
return self.title
class Permissions (models.Model):
class Mate :
db_table = 'Permissions'
id = models.AutoField(primary_key=True )
title = models.CharField(max_length=128 , verbose_name='标题' )
url = models.CharField(max_length=255 , verbose_name='正则模式下的url' )
name = models.CharField(max_length=255 ,verbose_name='权限的别名,用户控制前后端不分离模板的按钮可显示否' ,null=False ,unique=True )
menu = models.ForeignKey(blank=True , null=True , verbose_name='所属菜单' , to='Menu' , on_delete=models.CASCADE)
pid = models.ForeignKey(to='Permissions' , verbose_name='不能成为2级菜单的权限关联到可以成为2级菜单下面 归属关系' , related_name='parents' ,
on_delete=models.SET_NULL, null=True , blank=True )
def __str__ (self ):
return self.title
class Role (models.Model):
class Mate :
db_table = 'Role'
id = models.AutoField(primary_key=True )
role_name = models.CharField(max_length=255 , verbose_name='角色名称' )
permissions = models.ManyToManyField(to='Permissions' , db_table='Role_Permissions' , verbose_name='对多对权限角色报绑定字段' ,
blank=True )
def __str__ (self ):
return self.role_name
class User (models.Model):
class Mate :
db_table = 'User'
id = models.AutoField(primary_key=True )
user_name = models.CharField(max_length=128 , verbose_name='账户' )
password = models.CharField(max_length=128 , verbose_name='密码' )
user_email = models.CharField(max_length=128 , verbose_name='邮箱' )
role = models.ManyToManyField(to='Role' , db_table='User_Role' , verbose_name='多对多用户与角色绑定字段' )
def __str__ (self ):
return self.user_name
用户登录权限初始化修改
from rbac.config import permissions_key, menu_session_name
def init_permission (user_obj, request ):
'''
在登录时导入函数,进行权限的初始化(当前用户权限记录)
user_obj: 用户对象,通过orm 获取用户的权限列表
request: 请求对象,将权限存放在session中
'''
permissions_list = user_obj.role.filter (permissions__isnull=False ).values(
"permissions__id" ,
'permissions__url' ,
'permissions__title' ,
'permissions__menu__id' ,
'permissions__menu__title' ,
"permissions__pid__id" ,
"permissions__pid__title" ,
"permissions__pid__url" ,
"permissions__name"
).distinct()
permissions_dict = {}
menu_dict = {}
for item in permissions_list:
1. 权限的全部有用的参数组成一个字典
children_menu = {
"id" : item.get('permissions__id' ),
'permissions_url' : item.get('permissions__url' ),
"permissions_title" : item.get('permissions__title' ),
"pid" : item.get('permissions__pid__id' ),
"ptitle" : item.get('permissions__pid__title' ),
"purl" : item.get('permissions__pid__url' )
}
2. 将权限添加到权限中
permissions_dict[item.get('permissions__name' )] = children_menu
3. 构建1 级菜单与2 级菜单的数据结构
one_menu_id = item.get('permissions__menu__id' )
'''
-- 具体逻辑
3.1.如果没有menu_id 直接跳过(不能成为2级菜单)
3.2.如果当前的menu_id 存在构建的结构中 就将二级菜单进行append添加children中
3.3.如果当前的menu_id 不在 menu_dict 结构中就进行构建
'''
if not one_menu_id:
continue
if one_menu_id in menu_dict:
menu_dict.get(one_menu_id).get('children' ).append(children_menu)
else :
menu_dict[one_menu_id] = {
"permissions_menu_title" : item.get('permissions__menu__title' ),
'children' : [children_menu, ]
}
request.session[menu_session_name] = menu_dict
request.session[permissions_key] = permissions_dict
中间件修改
需要进行修改
from django.utils.deprecation import MiddlewareMixin
import re
from django.http import JsonResponse
from rbac.config import permissions_key, valid_url_list
class RbacMiddlewareMixin (MiddlewareMixin ):
'''权限中间件认证功能(django 中间件)'''
def process_request (self, request ):
'''
当用户请求时,进行触发
1.获取当前用户请求的url地址
2.获取当前用户在session中保存的权限列表
3.匹配权限信息
'''
current_url = request.path_info.replace('/' , '' , 1 )
for i in valid_url_list:
if re.match(i, current_url):
return None
permissions_dict = request.session.get(permissions_key)
if not permissions_dict:
return JsonResponse({"code" : 403 , 'error' : '当前用户未登录,请进行登录后再访问' })
flag = False
navigation_list = [
{"title" : "首页" , "url" : "#" }
]
for url in permissions_dict.values():
re_url = f"^{url.get('permissions_url' )} $"
if re.match(re_url, current_url):
request.menu_id = url.get('pid' ) or url.get('id' )
if url.get('pid' ):
navigation_list.extend(
[{"title" : url.get('ptitle' ), "url" : url.get('purl' )},
{"title" : url.get('permissions_title' ), "url" : url.get('permissions_url' )}]
)
else :
navigation_list.extend([
{"title" : url.get('permissions_title' ), "url" : url.get('permissions_url' )}
])
request.navigation_list = navigation_list
flag = True
if not flag:
return JsonResponse({"code" : 403 , 'error' : '当前用户没有权限,请让管理员添加权限!' })
设置模板方法filter
@register.filter
def has_permissions (request, name ):
'''
特点:
1.最多接受两个参数
2.可以作为模板的判断
3.模板使用 需要先lode 在进行使用
4.传参 第一个参数|has_permissions:"第二个参数"
'''
if name in request.session.get(permissions_key):
return True
return False
html中使用
{% load menu %}
{% if request| has_permissions:"payment_edit" %}
<a style="color: #333333;" href="/payment/edit/{{ row.id }}/" >
<i class ="fa fa-edit" aria-hidden="true" ></i>
</a>
{% endif %}
上述总结
1 .权限控制(登录后获取权限,再次访问后通过中间件进行判断)
2 .动态菜单(在中间件中将动态菜单进行生成)
3 .权限的控制到按钮级别(根据权限在html页面判断当前权限是否存在,选择隐藏与显示)
权限功能分配
用户创建 删除 查询 修改
角色创建 修改 查询 删除
权限创建 修改 查询 删除
角色 分配 权限
用户 分配 角色
1. 创建url 角色[单表] 增删改查 显示绑定的权限内容
2. 创建url 用户[单表] 正删改查 显示绑定的角色内容
3. 创建url 菜单[单表] 增删改查
4. 创建url 权限[单表] 增删改查 显示自关联[关联表中可以作为二级菜单] 显示绑定1 级菜单
最需要注意的:
1. 这几张表的之间的关系,
2. 如果是前后端不分离的情况下,前端框架的路由与数据库中菜单表的关系,路由与后端的权限表api接口的关系进行绑定,前端框架的循环展示当前用户的菜单以及菜单下的2 级菜单(前端框架的路由),点击2 级菜单,就会从后端api接口中获取数据。进行保定关系设置。
前后端分离状态下的:https://www.yuque.com/zhanghaofei/blog/xrpz9p权限思路
任务分解
1 .角色管理 单表操作 增删改查
1 .不分离(modlform)使用
2 .反向生成 namespace 和 name 的反向生成url
3 .django 模板查找顺序 按照注册app的顺序一个一个找
2 .用户管理 单表操作 增删改查
1 .不分离(modlform)使用
2 .反向生成 namespace 和 name 的反向生成url
3 .django 模板查找顺序 按照注册app的顺序一个一个找
3 .菜单表与权限表(菜单与权限管理) 2 张表的增删改查
1 .需要展示一个1 级菜单的添加删除修改编辑
2 .在添加修改中需要进行绑定权限表中url进行二级菜单
3 .同时二级菜单需要绑定非菜单的权限url
发现项目中权限django
批量的添加权限,发现项目中得权限(url)
前后端不分离:
1. modelform 单个表单的添加操作
2. modelformset 多个表单添加操作验证 批量操作进行使用
前后端分离:
一个api接口将数据库中得已经记录的url与项目中得url进行对比显示在前端即可
通过框架内部的方法获取全部项目下的url进行处理与数据库中进行对比获取剩下的
from collections import OrderedDict
from django.conf import settings
from django.utils.module_loading import import_string
from django.urls import URLResolver, URLPattern
def recursion_urls (pre_namespace, pre_url, urlpatterns, url_ordered_dict ):
"""
# url的递归操作
pre_namespace : 路由分发的namespace
per_url:路由分发的路由
urlpatterns:路由关系
url_ordered_dict:用来保存获取的所有的路由
for item in urlpatterns:
item 是每一个url url.name 获取url别名 item.pattern 获取url对象
item.pattern._regex # 获取url
"""
for item in urlpatterns:
if isinstance (item, URLPattern):
if not item.name:
name = str (item.pattern).replace('^' , "" ).replace('$' , '' )
elif pre_namespace:
name = "%s:%s" % (pre_namespace, item.name)
else :
name = item.name
url = pre_url + str (item.pattern)
url = url.replace('^' , "" ).replace('$' , '' )
url_ordered_dict[name] = url
elif isinstance (item, URLResolver):
if pre_namespace:
if item.namespace:
namespace = "%s:%s" % (pre_namespace, item.namespace)
else :
namespace = pre_namespace
else :
if item.namespace:
namespace = item.namespace
else :
namespace = None
recursion_urls(namespace, pre_url + str (item.pattern), item.url_patterns, url_ordered_dict)
def get_all_url_dict ():
'''获取全部的项目的路由(需要有name别名)'''
url_ordered_dict = OrderedDict()
md = import_string(settings.ROOT_URLCONF)
recursion_urls(None , '/' , md.urlpatterns, url_ordered_dict)
return url_ordered_dict
def multi_permissions (request ):
'''
获取项目中得全部的url
'''
a = get_all_url_dict()
for k,v in a.items():
print (k,v)
return HttpResponse('ok' )
总结前后端分离
1. 当用户登录后获取权限(用户登录页面白名单)
1.1 . 当用户登录初始化数据库中得权限存储到session或者缓存中
2. 当用户在进行访问时
2.1 通过中间件 从缓存中获取初始化的权限信息 无权访问返回内容 与通过 返回的内容
权限需要根据项目中url而定
每一个url相当于一个接口
vue --- django
vue 有自己的路由
django 有自己的路由(是接口数据)
数据库表结构:
1. 权限表 api接口url
2. 角色表 角色绑定的权限
3. 用户表 用户绑定的角色
4. 菜单表
5. vue 前端路由表
菜单表
nenu_name : 菜单表的名称
vue 前端路由表
url: 前端的路由信息
menu_id : 与菜单表绑定的关系
权限表
url : 后端的接口
vue 前端路由_id
角色表
权限表_id :拥有的权限信息
用户表
角色表_id : 拥有那些角色
页面显示
'''
用户管理: 一级菜单(数据库菜单表生成的)
用户列表 二级菜单(前端的路由渲染的)
权限列表 二级菜单(前端的路由渲染的)
角色列表 二级菜单(前端的路由渲染的)
....
'''
数据显示(在前端页面进行循环展示侧边栏菜单)
[
{
'title' :'用户管理' ,"id" :"1作为渲染菜单的唯一标识" ,
"child" :[
{'title' :'用户列表' ,"url" :'url/list/' ,},
{'title' :'权限列表' ,"url" :'url/list/' ,},
{'title' :'角色列表' ,"url" :'url/list/' ,},
]
}
]
用户登录后根据角色找到权限信息
权限找到前端页面的显示路由,根据路由找到1 级菜单信息
进行渲染操作
前端页面显示要与后端api接口做归属关系
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步