赞
踩
最近自己把以前的运维平台系统重新写了一遍,优化了框架和功能,之前是使用的django自带的登录验证装饰器,虽然可以实现登录验证的功能,但是没有办法做到权限的限制,于是参考了博客大神们的文章,自己尝试改了一个rbac的权限系统,作此记录方便日后查阅。
功能:
登录验证(非登录用户误操作权限)
动态菜单加载(不同用户展示不同菜单)
请求url的鉴权(根据用户的权限是识别请求url是否有权限)
django-admin startapp rbac
根据权限思路创建用户UserInfo,角色Role,权限Permission(菜单)三个模型models 用户为平台用户
角色规定了每个具体的用户拥有的具体权限 用户和角色是多对多关系,一个用户可以是多个角色,一个角色也可以包含多个用户
权限表定义了某个具体的权限的一些内容,是否为一级菜单,是菜单还是API接口,菜单的url连接,应用图标等
角色和权限是多对多关系,一个角色可以拥有多个菜单,一个菜单也可以是同事属于多个角色
from django.db import models # Create your models here. class Permission(models.Model): """ 权限 """ title = models.CharField(max_length=32) url = models.CharField(max_length=128, null=True,blank=True) topid = models.IntegerField(default=0) icon = models.CharField(max_length=52, null=True, default='layui-icon-app') target = models.CharField(max_length=32, null=True, default='_self') status = models.IntegerField(default="1") type = models.IntegerField(default="1") #权限的类型,1为显示菜单,2为非显示请求接口 parent = models.ForeignKey("permission", null=True, blank=True, on_delete=models.CASCADE) def __str__(self): # 显示带菜单前缀的权限 return self.title class Meta: verbose_name = '权限菜单' verbose_name_plural = '权限菜单' class Role(models.Model): """ 角色:绑定权限 """ title = models.CharField(max_length=32, unique=True) permissions = models.ManyToManyField("permission") # 定义角色和权限的多对多关系 def __str__(self): return self.title class Meta: verbose_name = '角色' verbose_name_plural = '角色' class UserInfo(models.Model): """ 用户:划分角色 """ username = models.CharField(max_length=32) password = models.CharField(max_length=64) nickname = models.CharField(max_length=32) status = models.BooleanField(default=True) email = models.EmailField(null=True) signature = models.CharField(max_length=32,default="我不知道我是瓶子,还是瓶子里的一滴水",null=True) roles = models.ManyToManyField("Role") re_time = models.DateTimeField(auto_now_add=True) #注册时间 last_login = models.DateTimeField(auto_now=True) #最后登录 # 定义用户和角色的多对多关系 def __str__(self): return self.nickname class Meta: verbose_name = '用户' verbose_name_plural = '用户'
from django.contrib import admin from .models import Permission,Role,UserInfo,Log_history class RoleAdmin(admin.ModelAdmin): list_display = ["title","权限"] def 权限(self, obj): return [bt.title for bt in obj.permissions.all()] filter_horizontal = ('permissions',) class UserInfoAdmin(admin.ModelAdmin): list_display = ["username","角色","status","nickname","signature","last_login"] def 角色(self, obj): return [a.title for a in obj.roles.all()] filter_horizontal = ('roles',) class PermissionAdmin(admin.ModelAdmin): list_display = ["title","parent","url","topid","icon","status","type"] admin.site.register(Role,RoleAdmin) admin.site.register(UserInfo,UserInfoAdmin) admin.site.register(Permission,PermissionAdmin)
写一个初始化权限函数,在每次登陆时调用,赋予登录用户相关权限。 这里参考了相关大佬的文档进行了二次修改
在rbacapp下创建一个service目录,再新建一个init_permission.py
#这里的user_obj是到时候登录函数调用时赋值过来的用户对象, def init_permission(request, user_obj): """ 初始化用户权限, 写入session :param request: :param user_obj: :return: """ #通过反向引用关联的模型的字段最终转化为权限表里的数据,字典去重, permission_item_list = user_obj.roles.values( 'permissions__title', 'permissions__id', 'permissions__url', 'permissions__icon', 'permissions__type', 'permissions__parent', ).distinct() # print(permission_item_list) check_url_list = [] child_list = [] parant_list = [] userinfo = user_obj.username # print("昵称:"+userinfo) # 用户权限url列表,--> 用于中间件验证用户请求是否具有权限 for item in permission_item_list: #url不为空则视为子菜单或者API接口 if not item['permissions__url'] is None: check_url_list.append(item['permissions__url']) # 说明是子菜单 if item['permissions__type'] == 1: child_list.append(item) # url为空。为父菜单 if item['permissions__parent'] is None: # 说明是菜单项 if item['permissions__type'] == 1: parant_list.append(item) # 注:session在存储时,会先对数据进行序列化,因此对于Queryset对象写入session,加list()转为可序列化对象 request.session["check_url_list"] = check_url_list # 保存 权限菜单 和所有 菜单;用户登录后作菜单展示用 request.session["parant_list"] = parant_list # 保存二级菜单列表 request.session["child_list"]= child_list # 保存用户信息 request.session["userinfo"] = userinfo
##因为我这里只准备设计二级菜单,如果到时候有三级以上菜单,可以把子菜单child_list去掉,
在parant_list下做多次嵌套循环来定义每个父菜单下的多级子菜单。
编写自定义中间件用于鉴权,用户每次请求时都会区进行鉴权根据session里的参数来判断用户是否登录
以及用户的权限,如果不符合跳转到登录页等操作
在rbac的app目录下创建middleware目录。在里面创建rbac.py的函数用作鉴权中间件
rbac.py
from django.conf import settings from django.shortcuts import HttpResponse, redirect import re class MiddlewareMixin(object): def __init__(self, get_response=None): self.get_response = get_response super(MiddlewareMixin, self).__init__() def __call__(self, request): response = None if hasattr(self, 'process_request'): response = self.process_request(request) if not response: response = self.get_response(request) if hasattr(self, 'process_response'): response = self.process_response(request, response) return response class RbacMiddleware(MiddlewareMixin): """ 检查用户的url请求是否是其权限范围内 """ def process_request(self, request): request_url = request.path_info # check_url_list = request.session.get(settings.CHECK_URL_LIST) check_url_list = request.session.get("check_url_list") print("调用鉴权插件.") # 如果请求url在白名单,放行 for url in settings.SAFE_URL: # print("检测是否为安全组url") if re.match(url, request_url): return None # 如果未取到permission_url, 重定向至登录;为了可移植性,将登录url写入配置 if not check_url_list: # print("检测是否登录及具有权限列表") return redirect(settings.LOGIN_URL) # 循环permission_url,作为正则,匹配用户request_url # 正则应该进行一些限定,以处理:/user/ -- /user/add/匹配成功的情况 flag = False for url in check_url_list: # print("检查具体的请求url是否在权限内") url_pattern = settings.REGEX_URL.format(url=url) if re.match(url_pattern, request_url): flag = True break if flag: return None else: # 如果是调试模式,显示可访问url # if settings.DEBUG: # info ='<br/>' + ( '<br/>'.join(check_url_list)) # return HttpResponse('无权限,请尝试访问以下地址:%s' %info) # else: # return redirect(settings.LOGIN_URL) return HttpResponse('无权限访问')
将鉴权中间件加入setting中
MIDDLEWARE = [
‘django.middleware.security.SecurityMiddleware’,
‘django.contrib.sessions.middleware.SessionMiddleware’,
‘corsheaders.middleware.CorsMiddleware’,
‘django.middleware.common.CommonMiddleware’,
# ‘django.middleware.csrf.CsrfViewMiddleware’,
‘django.contrib.auth.middleware.AuthenticationMiddleware’,
‘django.contrib.messages.middleware.MessageMiddleware’,
‘django.middleware.clickjacking.XFrameOptionsMiddleware’,
‘rbac.middleware.rbac.RbacMiddleware’ # 加入自定义的中间件到最后
]
注意,由于业务的逻辑是登录调用初始化权限函数去赋值权限存放到session中,然后在用户请求时,鉴权插件会去session中获取相关权限参数经行鉴权,所以鉴权中间件需要放到session中间件之后,不然会出现获取不到session的情况,一搬放到MIDDLEWARE 的最后就可以。
鉴权中间件里有设计到安全组url以及登录url,这里把他配置到setting文件中,
安全组url 用户配置一些默认可以访问的url,不需要经过鉴权插件判断鉴权如主页,登录登出页等。
登录
在setting.py中任意位置配置:
#配置url权限白名单
SAFE_URL = [
r'/login/',
'/logout/',
'/index/',
'/register',
'/admin/.*',
'/test/',
'^/rbac/',
#配置登录
LOGIN_URL = '/login/'
这样,基本的权限框架就出来了,这里简单的写一下主页和登录页,
主页的菜单按照之前的设想时需要动态展示的,由于数据存放在session里,所以在前端模板里直接调用session的值去循环就可以。
关于菜单的显示是在base.html的模板里的,这里贴出来菜单处的代码,用的是layui的admin模板
... <div class="layui-side layui-bg-black"> <div class="layui-side-scroll"> <!-- 左侧导航区域(可配合layui已有的垂直导航) --> <ul class="layui-nav layui-nav-tree " lay-filter="test"> {% for parant in request.session.parant_list %} <li class="layui-nav-item layui-nav-itemed layui-menu-item-up"> <a class="" href="#"><i class="layui-icon {{ parant.permissions__icon }}"></i> <span > {{ parant.permissions__title }} </span></a> {% for child in request.session.child_list %} {% if child.permissions__parent == parant.permissions__id%} <dl class="layui-nav-child"> <dd><a href="{{ child.permissions__url }}"><i class="layui-icon {{ child.permissions__icon }}"></i> <span > {{ child.permissions__title }}</span></a></dd> </dl> {% endif %} {% endfor %} </li> {% endfor %} </ul> </div> </div> ...
主页函数
from django.shortcuts import render,redirect,HttpResponse from django.conf import settings from django.http import HttpResponse from ..models import UserInfo from web.models import Log_history import json def index(request): try : user = request.session.get("userinfo") if user == None: return redirect('/login/') print(user) print("index处") obj = UserInfo.objects.all() return render(request, "rbac/index.html",{"obj":obj}) except : print("22222") return redirect('/login/')
登录函数
from django.shortcuts import render, redirect, HttpResponse from ..models import UserInfo,Log_history from ..service.init_permission import init_permission from django.utils import timezone import time # from yunwei.web.models import Log_history #登录 #获取当前IP函数 def get_ip(request): x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0]#所以这里是真实的ip else: ip = request.META.get('REMOTE_ADDR')#这里获得代理ip return ip def login(request): if request.method == "GET": return render(request, "rbac/login.html") else: username = request.POST.get('username') password = request.POST.get('password') user_obj = UserInfo.objects.filter(username=username, password=password).first() if not user_obj: # 获取客户端ip和当前时间 create_time = timezone.now() print(create_time) ip = get_ip(request) print(ip) obj = Log_history.objects.create(log_name=username, log_ip=ip, log_time=create_time, log_status="登陆失败") return render(request, "rbac/login.html", {'error': '用户名或密码错误!'}) else: init_permission(request, user_obj) #调用init_permission,初始化权限 print("调用init_permission,初始化权限") #获取客户端ip和当前时间 create_time = timezone.now() print(create_time) ip = get_ip(request) print(ip) #保存记录到历史记录表格 obj = Log_history.objects.create(log_name=username, log_ip=ip,log_time=create_time,log_status="登陆成功") #更新用户最后登录时间 a = UserInfo.objects.get(username=username) a.save() return redirect('/index/')
到这里基本已经实现登录鉴权功能,当用户去访问具体的url时,会调用鉴权中间件,如果没登陆会获取不到session中的值会跳转到登录,如果用户的权限url里找不到请求的url,也会跳转到登录页,
跳转到登录·界面后,会调用初始化权限函数去赋值权限
不同的用户拥有的菜单是根据session动态获取的。到此基本完成,做此纪录。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。