阅读:51
最近自己把以前的运维平台系统重新写了一遍,优化了框架和功能,之前是使用的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动态获取的。到此基本完成,做此纪录。