django实现rbac权限管理系统

     阅读:24

最近自己把以前的运维平台系统重新写了一遍,优化了框架和功能,之前是使用的django自带的登录验证装饰器,虽然可以实现登录验证的功能,但是没有办法做到权限的限制,于是参考了博客大神们的文章,自己尝试改了一个rbac的权限系统,作此记录方便日后查阅。
功能:
登录验证(非登录用户误操作权限)
动态菜单加载(不同用户展示不同菜单)
请求url的鉴权(根据用户的权限是识别请求url是否有权限)

创建一个单独的app用于权限系统,这里叫rbac,

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 = '用户'

配置模型加入admin管理后台及其显示格式

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以及登录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 >&nbsp;&nbsp;{{ 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 >&nbsp;{{ 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动态获取的。到此基本完成,做此纪录。