django的权限组件
Posted zezhou
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了django的权限组件相关的知识,希望对你有一定的参考价值。
个人思路设计,没啥太多经验,轻喷
权限的作用:
不同用户,根据不同的权限,显示不同的页面内容。
# 由于用户的权限路径可能重复,所以通过角色对应路径,可以重用,只需要用户指定角色,角色对应路径,就可以很方便(只要限制角色的权限即可,用户指定不同的角色,达到权限重用)
权限要关注的几个点:
(1) 权限注入
在登陆成功的时候注入权限,该测试项目,我按照我中间件认证、动态菜单、模板认证权限需要内容做的权限注入。
# 权限注入的目的是后续更轻松的操作,也可以不注入权限,通过登陆成功的id去看权限也可以
(2) 中间件认证
有人可能疑惑为啥要中间件,前端页面根据对应权限渲染,其实已经达到了权限分配的目的,没有权限不能请求到后端非自己权限的内容了。
中间件目的是:
限制伪装前端人过来请求,由于伪装前端进行后端请求,如果不做中间件认证就会被偷鸡。
(3) 动态菜单
动态菜单的意义在于,当用户请求的页面,有一个菜单下,所有权限都没有的时候,就不显示该菜单。
(4) 前端页面的模板判断
这里疑惑是权限认证是前端做还是后端做。
后端做这件事情好:
现实中的例子:
一个完整的东西,按实际来讲肯定是要让拥有者交给用户手中的。
实现方式:
后端通过模板判断方式实现,通过权限路径别名,前端页面每个权限那块写死一个值进行用户的权限认证判断实现。
前端做的坏处:
(1) 如果前端做这个事情,肯定是在用户机器上运行到js代码那块才可以,权限用到的标签什么的都是隐藏状态,从后端获取到权限在按权限,进行显示,一个会代码的人,就可以看到,不安全
(2) 增加了不必要的运行时间,用户权限低,本身,就看不到某块内容,前端还是要加载权限所有内容,不好
(3) 前端做权限判断,这件事情太麻烦了
后端做的好处:
后端拿到页面,进行判断,通过判断的部分,进行部分页面内容返回,增加了安全性,和不必要的麻烦
二级菜单权限,我的写法流程:
1. 设计表结构
一开始,表结构的字段没考虑那么的多,表结构字段都是需要用到什么,临时加,即测试的。
所以根据作用目前完整表结构设计就如下:
from django.db import models from django.contrib.auth.models import AbstractUser # 继承了django内置的user表 class UserInfo(AbstractUser): # 用户对应的角色 role = models.ManyToManyField(to="Role") class Meta: db_table = ‘userInfo‘ class Role(models.Model): # 角色名,权限配置页面用 role_name = models.CharField(max_length=20, blank=False) # 角色对应路径,因为角色只需要关心这个角色有什么权限路径即可,菜单是权限路径该关系的,所以表结构这样设计 path = models.ManyToManyField(to=‘Path‘) class Meta: db_table = ‘role‘ # 动态菜单用 class Menu(models.Model): # 权限配置页面渲染菜单名称的 menu_name = models.CharField(max_length=30, blank=False) # 动态菜单的请求路径,由于我页面是用的Iframe标签,所以需要有个api地址获取到页面 request_path = models.CharField(max_length=100) # 用于动态菜单渲染,如果值为False表示不渲染至用户显示页面 is_menu = models.BooleanField(default=1) # 二级菜单指向一级菜单,如果是一级菜单pid字段为空 pid = models.ForeignKey(to=‘self‘, on_delete=False, null=True) class Meta: db_table = ‘menu‘ class Path(models.Model): # api接口的地址,用于中间件判断 auth_path = models.CharField(max_length=100, blank=False) # 用作一个api接口多种请求方式的限制,api接口设计规范,get获取数据,post提交数据,put更新数据... 权限的细控制 method = models.CharField(max_length=10, blank=False, default=‘GET‘) # 用作前端模板判断该用户是否有这个权限,通过别名,防止api接口变化,不需要修改模板页面的作用 alias = models.CharField(max_length=20) # 用作权限配置页面显示权限路径的名称 path_name = models.CharField(max_length=30) # 权限对应的二级菜单是哪个 menu = models.ForeignKey(to=‘Menu‘, on_delete=False) class Meta: db_table = ‘path‘
2. 初始数据库数据
有了表结构,肯定是要初始一些初始权限、菜单名、角色、用户的。
文件位置:
在项目的rbac文件夹下的init_data.py。
文件内容:
import os import django project_name = ‘test_qx‘ # 不同项目需要修改 os.environ.setdefault("DJANGO_SETTINGS_MODULE", project_name + ".settings") django.setup() from rbac.models import * menu_data = [ {"id": 1, "menu_name": ‘非菜单-1‘, "is_menu": False}, {"id": 2, "menu_name": ‘学生管理-1‘}, {"id": 3, "menu_name": ‘书籍管理-1‘}, {"id": 4, "menu_name": ‘学生管理‘, "request_path": "/student_page/", "pid_id": 2}, {"id": 5, "menu_name": ‘获取学生信息‘, "request_path": "/student/", "pid_id": 2}, {"id": 6, "menu_name": ‘书籍管理‘, "request_path": "/book_page/", "pid_id": 3}, {"id": 7, "menu_name": ‘非菜单‘, "is_menu": False, "pid_id": 1}, ] path_data = [ {"id": 1, "auth_path": ‘/student/$‘, "method": ‘GET‘, "alias": ‘get_students‘, "path_name": ‘获取学生‘, "menu_id": 4}, {"id": 2, "auth_path": ‘/student/$‘, "method": ‘POST‘, "alias": ‘add_student‘, "path_name": ‘添加学生‘, "menu_id": 4}, {"id": 3, "auth_path": ‘/student/$‘, "method": ‘PUT‘, "alias": ‘edit_student‘, "path_name": ‘编辑学生‘, "menu_id": 4}, {"id": 4, "auth_path": ‘/student/$‘, "method": ‘DELETE‘, "alias": ‘delete_student‘, "path_name": ‘删除学生‘, "menu_id": 4}, {"id": 5, "auth_path": ‘/book/$‘, "method": ‘GET‘, "alias": ‘get_books‘, "path_name": ‘获取书籍‘, "menu_id": 6}, {"id": 6, "auth_path": ‘/book/$‘, "method": ‘POST‘, "alias": ‘add_book‘, "path_name": ‘添加书籍‘, "menu_id": 6}, {"id": 7, "auth_path": ‘/book/$‘, "method": ‘PUT‘, "alias": ‘edit_book‘, "path_name": ‘编辑书籍‘, "menu_id": 6}, {"id": 8, "auth_path": ‘/book/$‘, "method": ‘DELETE‘, "alias": ‘delete_book‘, "path_name": ‘删除书籍‘, "menu_id": 6}, {"id": 9, "auth_path": ‘/index/$‘, "path_name": ‘访问首页‘, "menu_id": 7}, {"id": 10, "auth_path": ‘/student_page/$‘, "path_name": ‘访问学生页面‘, "menu_id": 7}, {"id": 11, "auth_path": ‘/book_page/$‘, "path_name": ‘访问书籍页面‘, "menu_id": 7}, {"id": 12, "auth_path": ‘/logout/$‘, "path_name": ‘注销账户‘, "menu_id": 7}, # 修改权限得权限 {"id": 13, "auth_path": ‘/rbac/‘, "method": ‘*‘, "path_name": ‘修改权限‘, "menu_id": 7}, # 测试一级菜单下面俩个二级菜单 {"id": 14, "auth_path": ‘/student/$‘, "method": ‘GET‘, "alias": ‘get_students‘, "path_name": ‘获取学生‘, "menu_id": 5}, ] role_data = [ {"id": 1, "role_name": "管理员", "path_id": (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)}, {"id": 2, "role_name": "普通用户", "path_id": (1, 2, 3, 5, 6, 7, 9, 10, 11, 12, 14)}, ] admin_user_data = [ {"id": 1, "username": "admin", "password": "123456", "email": "test@qq.com"} ] # 添加菜单数据 for dic in menu_data: # 添加菜单 obj = Menu.objects.create(**dic) # 添加权限路径数据 for dic in path_data: Path.objects.create(**dic) # 添加角色数据 for dic in role_data: temp = dic path_ids = temp.pop("path_id") # 添加角色 obj = Role.objects.create(**temp) # 添加角色和权限路径的关系 Role.objects.get(id=obj.id).path.set(path_ids) # 添加管理员用户 for dic in admin_user_data: obj = UserInfo.objects.create_superuser(**dic) obj.role.set((1,)) # 用户关联管理员角色 print("执行完毕.")
3. 注册关联初始角色
# 创建用户数据,返回的用户对象 obj = UserInfo.objects.create_user(**dic) # 关联初始权限 role_id = Role.objects.get(role_name=‘普通用户‘).id UserInfo.objects.get(id=obj.id).role.set((role_id,))
# 参考写法,效率不高,我随便写的
4. 登陆成功注入权限
注入权限完全是为了中间件认证、动态菜单、前端模板认证时候使用,所有里面放的内容,自己舒服就好。
首先要注入什么内容,我会有个参照结构:
""" [ { "one_menu": "书籍管理-1", "is_menu": 1, # 为真表示是动态菜单要渲染得内容 "two_menu": [ { "menu": "书籍管理", "request_path": "/book_page/", "path": [ {"method": "get", "auth_path": "/book/", "alias": "get_books"}, {"method": "post", "auth_path": "/book/", "alias": "add_book"} ] }, { "menu": "获取数据", "request_path": "/book/", "path": [ {"method": "get", "auth_path": "/book/$", "alias": "get_books"}, ] }, ] }, { "one_menu": "学生管理-1", "is_menu": 1, "two_menu": [ { "menu": "学生管理", "request_path": "/student_page/", "path": [ {"method": "get", "auth_path": "/student/", "alias": "get_students"}, {"method": "post", "auth_path": "/student/", "alias": "add_student"} ] }, ] }, ] """
注:我这里套的结构深了点,有点不好理解些,其实可以菜单名和权限路径可以分到俩里面存的
代码写法:
all_path_data = [] # 查询用户都有什么权限路径,顺带出菜单,减少数据库交互,提升效率 values = UserInfo.objects.filter(id=request.user.id).values( ‘role__path__menu__pid__menu_name‘, ‘role__path__menu__menu_name‘, ‘role__path__menu__request_path‘, ‘role__path__menu__is_menu‘, ‘role__path__method‘, ‘role__path__auth_path‘, ‘role__path__alias‘, ) # print(values) for dic in values: one_menu = dic[‘role__path__menu__pid__menu_name‘] menu = dic[‘role__path__menu__menu_name‘] request_path = dic[‘role__path__menu__request_path‘] is_menu = dic[‘role__path__menu__is_menu‘] method = dic[‘role__path__method‘] auth_path = dic[‘role__path__auth_path‘] alias = dic[‘role__path__alias‘] for dic2 in all_path_data: if dic2["one_menu"] == one_menu: temp = { "menu": menu, "request_path": request_path, "path": [{"method": method, "auth_path": auth_path, "alias": alias}] } for dic3 in dic2["two_menu"]: if dic3["menu"] == menu: temp = {"method": method, "auth_path": auth_path, "alias": alias} dic3["path"].append(temp) # 正常添加完跳出,否则重复执行for-else里得内容 break else: # 二级菜单没有时候执行 dic2["two_menu"].append(temp) # 正常添加完跳出,否则重复执行for-else里得内容 break else: # 一级菜单没时候才会执行 temp = { "one_menu": one_menu, "is_menu": is_menu, "two_menu": [ { "menu": menu, "request_path": request_path, "path": [{"method": method, "auth_path": auth_path, "alias": alias}] } ] } all_path_data.append(temp) # print(all_path_data) # 添加权限路径到session request.session["auth"] = all_path_data
5. 中间件认证
防止伪造前端的人进行请求。
写法思路:
其实就是拿到所有二级菜单下得权限路径,进行请求方法、请求路径得判断,通过就返回None,就可以进入视图或下个中间件了。
中间件得介绍:
和装饰器一个道理,进入里面的内容,得先经过一个装饰器,只不过这里中间件是默认所有视图函数得装饰器。
process_request是进入视图前要进过的,再进入视图函数前,我进行权限认证,防止没权限得人不能进入其中拿到数据。
中间件代码:
import re from django.utils.deprecation import MiddlewareMixin from django.shortcuts import HttpResponse, redirect from test_qx.settings import white_list class MyMiddleware(MiddlewareMixin): def process_request(self, request): # 当前请求路径 now_path = request.path now_method = request.method # 白名单直接进入 for rule in white_list: if re.search(rule, now_path): return None # 判断是否进行了登陆 if not request.user.is_authenticated: return redirect(‘/login/‘) # 取出用户的路径 auth = request.session.get(‘auth‘, None) # 权限路径认证 if auth: # 进行循环判断 for one_menu_dic in auth: for two_menu_dic in one_menu_dic["two_menu"]: for dic in two_menu_dic["path"]: # 如果当前请求的内容,符合权限里的,就进入 if re.search(dic["auth_path"], now_path) and (dic["method"] == now_method or dic["method"] == ‘*‘): return None # 没权,则直接返回 return HttpResponse(‘not allow!‘)
注意:
(1) 我用到了白名单,以后不需要权限认证得、页面不需要用到权限认证得内容,请填入到白名单中,在settings中写的,其它中间件调用同一份白名单即可。
(2) 我path表里method字段为*表示所有请求方式都通过
(3) 请求路径是正则进行匹配通过得,所以path数据表里auth_path字段应该存正则表达式
6. 动态菜单
为了一个页面下所有权限都没有,就表示没必要渲染这个菜单,让用户点击进入,根据实际需求改把(页面没细分所有权限时候)
写法思路:
取出登陆注入得内容,进行循环渲染菜单。
写法:
7. 子页面得权限认证判断
根据权限是否显示该功能。
用到了django模板函数,为了达到内容复用,否则麻烦点,要视图下取出所有alias字段,进行判断,或,模板渲染那循环进行判断。
写法思路:
通过alias字段值,进行判断。
my_tag写法:
from django import template register = template.Library() # register和templatetags文件夹名是固定的名称 @register.filter def match_permission(request, value): # value是模板页面传得死值 # 从request中取出登陆出入得权限,和死值进行判断 auth = request.session["auth"] for one_menu_dic in auth: if not one_menu_dic["is_menu"]: continue for two_menu_dic in one_menu_dic["two_menu"]: for dic in two_menu_dic["path"]: if dic["alias"] == value: return True return False
子页面权限判断写法:
student.html为例:
注意:首先要导入my_tag文件,否则调用不到模板函数match_permission函数。
事件也要进行判断权限进行返回:
8. 完毕
到这权限得核心代码就都完毕了,之后就剩下权限配置页面了。
个人写的简单权限配置页面:
# 随便写的,只为达到个测试效果,个人觉得,权限核心代码可能就100多行,但是配置页面,繁华点可能就几千行了
测试项目代码:
一级菜单版本:
二级菜单版本:
以上是关于django的权限组件的主要内容,如果未能解决你的问题,请参考以下文章
django(权限认证)系统——第三方组件实现Object级别权限控制