Django权限管理实现

概述

RBAC(Role-Based Access Control,基于角色的访问控制),通过角色绑定权限,然后给用户划分角色。在web应用中,可以将权限理解为url,一个权限对应一个url。在实际应用中,url是依附在菜单下的,每个菜单下又可以有子菜单,但最终都会指向一个url,点击这个url,通过Django路由系统执行一个视图函数,来完成某种操作。比如一个简单后台管理系统,菜单可以大致分为以下几块:项目管理、设备管理、权限管理等等。普通用户登录系统后,不会显示设备管理、权限管理菜单。

设计表关系

基于上述分析,在设计表关系时,起码要有4张表:用户,角色,权限,菜单。
用户可以绑定多个角色,从而实现灵活的权限组合 :用户和角色,多对多关系。
每个角色下,绑定多个权限,一个权限也可以属于多个角色:角色和权限,多对多关系。
一个权限附属在一个菜单下,一个菜单下可以有多个权限:菜单和权限:多对一关系。
一个菜单下可能有多个子菜单,也可能有一个父菜单:菜单和菜单是自引用关系
其中角色和权限、用户和角色,是两个多对多关系,由Django自动生成另外两种关联表。因此一共会产生6张表,用来实现权限管理。

下面在项目下新建rbac应用,在该应用的models.py中来定义这几张表:
# _*_ encoding:utf-8 _*_
from django.db import models
from django.contrib.auth.models import AbstractUser

class Menu(models.Model):
    """
    菜单
    """
    title = models.CharField(max_length=32, unique=True,verbose_name=u'菜单')
    icon = models.CharField(max_length=10,verbose_name=u'菜单图标',null=True, blank=True)
    parent = models.ForeignKey("Menu", null=True, blank=True,on_delete=models.CASCADE)
    # 定义菜单间的自引用关系
    # 权限url 在 菜单下;菜单可以有父级菜单;还要支持用户创建菜单,因此需要定义parent字段(parent_id)
    # blank=True 意味着在后台管理中填写可以为空,根菜单没有父级菜单

    def __str__(self):
        # 显示层级菜单
        title_list = [self.title]
        p = self.parent
        while p:
            title_list.insert(0, p.title)
            p = p.parent
        return '-'.join(title_list)

    class Meta:
        verbose_name = u"菜单"
        verbose_name_plural = verbose_name


class Permission(models.Model):
    """
    权限
    """
    title = models.CharField(max_length=32, unique=True,verbose_name = u"权限")
    url = models.CharField(max_length=128, unique=True)
    icon = models.CharField(max_length=10, verbose_name=u'权限图标', null=True, blank=True)
    menu = models.ForeignKey("Menu", null=True, blank=True,on_delete=models.CASCADE)

    def __str__(self):
        # 显示带菜单前缀的权限
        return '{menu}---{permission}'.format(menu=self.menu, permission=self.title)

    class Meta:
        verbose_name = u"权限"
        verbose_name_plural = verbose_name


class Role(models.Model):
    """
    角色:绑定权限
    """
    title = models.CharField(max_length=32, unique=True,verbose_name = u"角色")

    permissions = models.ManyToManyField("Permission")
    # 定义角色和权限的多对多关系

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = u"角色"
        verbose_name_plural = verbose_name


class UserInfo(models.Model):
    """
    用户:划分角色
    """
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64)
    real_name = models.CharField(max_length=50, verbose_name=u"真实姓名", default="")
    mobile = models.CharField(max_length=11, null=True, blank=True, verbose_name=u"手机号")
    image = models.ImageField(upload_to="img/",default=u"img/default.png", max_length=100,verbose_name=u"头像")
    roles = models.ManyToManyField(to='Role',verbose_name='角色')

    class Meta:
        verbose_name = u"用户"
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.username

权限初始化和验证

用户登录成功后,可以从数据库中取出该用户角色下对应的权限信息,并将这些信息写入session中。
所以每次用户的Http request过来后,服务端尝试从request.session中取出权限信息,如果为空,说明用户未登录,重定向至登录页面。否则说明已经登录(即权限信息已经写入request.session中),将用户请求的url与其权限信息进行匹配,匹配成功则允许访问,否则拦截请求。

第一步:提取用户权限信息,并写入session

  在rbac应用下新建一个文件夹service,写一个脚本init_permission.py用来执行初始化权限的操作:用户登录后,取出其权限及所属菜单信息,写入session中
from ..models import UserInfo, Menu


def init_permission(request, user_obj):
    """
    初始化用户权限, 写入session
    :param request: 
    :param user_obj: 
    :return: 
    """
    permission_item_list = user_obj.roles.values('permissions__url',
                                                 'permissions__title',
                                                 'permissions__icon',
                                                 'permissions__menu_id').distinct()
    permission_url_list = []  # 用户权限url列表,--> 用于中间件验证用户权限
    permission_menu_list = []  # 用户权限url所属菜单列表 [{"title":xxx, "url":xxx, "menu_id": xxx},{},]

    for item in permission_item_list:
        permission_url_list.append(item['permissions__url'])
        if item['permissions__menu_id']:
            temp = {"title": item['permissions__title'],
                    "url": item["permissions__url"],
                    "menu_id": item["permissions__menu_id"],
                    'icon':item["permissions__icon"]}
            permission_menu_list.append(temp)

    menu_list = list(Menu.objects.values('id', 'title', 'parent_id','icon'))
    # 注:session在存储时,会先对数据进行序列化,因此对于Queryset对象写入session, 加list()转为可序列化对象

    from django.conf import settings

    request.session[settings.SESSION_PERMISSION_URL_KEY] = permission_url_list

    # 保存 权限菜单 和所有 菜单
    request.session[settings.SESSION_MENU_KEY] = {
        settings.ALL_MENU_KEY: menu_list,
        settings.PERMISSION_MENU_KEY: permission_menu_list,
    }
在项目的settings中指定session保存权限信息的key:
# 定义session 键:
# 保存用户权限url列表
# 保存 权限菜单 和所有 菜单
SESSION_PERMISSION_URL_KEY = 'cool'

SESSION_MENU_KEY = 'awesome'
ALL_MENU_KEY = 'k1'
PERMISSION_MENU_KEY = 'k2'

这样,用户登录后,调用init_permission,即可完成初始化权限操作。而且即使修改了用户权限,每次重新登录后,调用该方法,都会更新权限信息。
新建应用app01,在settings.py中加入该应用。更改views.py名称为appviews.py,写入login、index方法。

from django.shortcuts import render
from rbac.models import UserInfo
from rbac.service.init_permission import init_permission
from django.shortcuts import HttpResponseRedirect

# Create your views here.

def login(request):
    if request.method == "GET":
        return render(request, "login.html")
    if request.method=="POST":
        username = request.POST.get('form-username')
        password = request.POST.get('form-password')

        user_obj = UserInfo.objects.filter(username=username, password=password).first()
        if user_obj:
            request.session["user_id"] = user_obj.pk
            request.session["Username"] = username
            init_permission(request, user_obj) #调用init_permission,初始化权限
            return HttpResponseRedirect("/index/")
        else:
            return render(request,"login.html",{"error":"Username and Password is error"})

def index(request):

    return render(request, 'index.html')

第二步:检查用户权限,控制访问

要在每次请求过来时检查用户权限,对于这种对请求作统一处理的需求,利用中间件完成。
我们在rbac应用下新建一个目录middleware,用来存放自定义中间件,新建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
        permission_url = request.session.get(settings.SESSION_PERMISSION_URL_KEY)
        print('访问url',request_url)
        print('权限--',permission_url)
        # 如果请求url在白名单,放行
        for url in settings.SAFE_URL:
            if re.match(url, request_url):
                return None

        # 如果未取到permission_url, 重定向至登录;为了可移植性,将登录url写入配置
        # 另外,Login必须设置白名单,否则访问login会反复重定向
        if not permission_url:
            return redirect(settings.LOGIN_URL)

        # 循环permission_url,作为正则,匹配用户request_url
        # 正则应该进行一些限定,以处理:/user/ -- /user/add/匹配成功的情况
        flag = False
        for url in permission_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 ='
' + ( '
'.join(permission_url)) return HttpResponse('无权限,请尝试访问以下地址:%s' %info) else: return HttpResponse('无权限访问')

说明:
1.有些访问不需要权限,或者在测试时,我们可以在settings中配置一个白名单;
2.将登录的url写入settings中,增强可移植性;
3.url本质是正则表达式,在匹配用户请求的url是否在其权限范围内时,需要作严格匹配,这个也可以在settings中配置
4.中间件定义完成后,加入settings中的MIDDLEWARE列表中最后面(加到前面可能还没有session信息)
settings,py中配置如下:

LOGIN_URL = '/login/'

REGEX_URL = r'^{url}$'  # url作严格匹配

# 配置url权限白名单
SAFE_URL = [
    r'/login/',
    '/admin/.*',
    '/test/',
    '/index/',
    '^/rbac/',
    '^/rbac/menus/',
]
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    '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',  #添加自定义中间件
]

菜单显示

用户登录后,应该根据其权限,显示其可以操作的菜单。前面我们我们已经将用户的权限和菜单信息保存在了request.session中,因此如何从中提取信息,并将其渲染成页面显示的菜单,就是接下来要解决的问题。
提取信息很简单,因为在用户登录后调用init_permission初始化权限时,已经将权限和菜单信息进行了初步处理,并写入了session,这里只需要通过key将信息取出来即可。
第一,只显示用户权限对应的菜单,因此不同用户看到的菜单可能是不一样的
第二,对用户当前访问的菜单下的url作展开显示,其余菜单折叠;
第三,菜单的层级是不确定的(而且,后面要实现权限的后台管理,允许管理员添加菜单和权限);

自定义标签

接下来我们通过自定义标签,来实现以上需求:
-它接收request参数,从中提取session保存的权限和菜单数据
-对数据作结构化处理
-将数据渲染为html字符串
我们在rabc应用的目录下新建templatetags目录,写一个脚本custom_tag.py,将数据处理部分和数据渲染部分抽象为两个函数。rbac_menu完成两部分操作,并加上自定义标签的装饰器。

from django import template
from django.conf import settings
import re, os
from django.utils.safestring import mark_safe

register = template.Library()


def get_structure_data(request):
    """处理菜单结构"""
    menu = request.session[settings.SESSION_MENU_KEY]
    all_menu = menu[settings.ALL_MENU_KEY]
    permission_url = menu[settings.PERMISSION_MENU_KEY]

    # 定制数据结构
    all_menu_dict = {}
    for item in all_menu:
        item['status'] = False
        item['open'] = False
        item['children'] = []
        all_menu_dict[item['id']] = item

    request_rul = request.path_info

    for url in permission_url:
        # 添加两个状态:显示 和 展开
        url['status'] = True
        pattern = url['url']
        if re.match(pattern, request_rul):
            url['open'] = True
        else:
            url['open'] = False

        # 将url添加到菜单下
        all_menu_dict[url['menu_id']]["children"].append(url)

        # 显示菜单:url 的菜单及上层菜单 status: true
        pid = url['menu_id']
        while pid:
            all_menu_dict[pid]['status'] = True
            pid = all_menu_dict[pid]['parent_id']

        # 展开url上层菜单:url['open'] = True, 其菜单及其父菜单open = True
        if url['open']:
            ppid = url['menu_id']
            while ppid:
                all_menu_dict[ppid]['open'] = True
                ppid = all_menu_dict[ppid]['parent_id']

    # 整理菜单层级结构:没有parent_id 的为根菜单, 并将有parent_id 的菜单项加入其父项的chidren内
    menu_data = []
    for i in all_menu_dict:
        if all_menu_dict[i]['parent_id']:
            pid = all_menu_dict[i]['parent_id']
            parent_menu = all_menu_dict[pid]
            parent_menu['children'].append(all_menu_dict[i])
        else:
            menu_data.append(all_menu_dict[i])

    return menu_data


def get_menu_html(menu_data):
    """显示:菜单 + [子菜单] + 权限(url)"""
    option_str = """
         
  • {menu_icon} {menu_title}
    {sub_menu}
  • """ url_str = """ {permission_icon} {permission_title} """ menu_html = '' for item in menu_data: if not item['status']: # 如果用户权限不在某个菜单下,即item['status']=False, 不显示 continue else: if item.get('url'): # 说明循环到了菜单最里层的url menu_html += url_str.format(permission_url=item['url'], active="rbac-active" if item['open'] else "", permission_icon=item['icon'], permission_title=item['title']) else: if item.get('children'): sub_menu = get_menu_html(item['children']) else: sub_menu = "" menu_html += option_str.format(menu_icon=item['icon'], menu_title=item['title'], sub_menu=sub_menu, display="" if item['open'] else "rbac-hide", status="open" if item['open'] else "close") return menu_html @register.simple_tag def rbac_menu(request): """ 显示多级菜单:请求过来 -- 拿到session中的菜单,权限数据 -- 处理数据 -- 作显示 返回多级菜单:数据处理部分抽象出来由单独的函数处理;渲染部分也抽象出来由单独函数处理 :param request: :return: """ menu_data = get_structure_data(request) menu_html = get_menu_html(menu_data) return mark_safe(menu_html) # 因为标签无法使用safe过滤器,这里用mark_safe函数来实现

    样式和JS文件处理

    在渲染菜单时会用到自定义的css和js文件,这些也应该打包好,保证rbac的可迁移性。因此,在这个自定义标签的脚本中,额外定义两个标签,用来加载css和js文件。

    @register.simple_tag
    def rbac_css():
        """
        rabc要用到的css文件路径,并读取返回;注意返回字符串用mark_safe,否则传到模板会转义
        :return: 
        """
        css_path = os.path.join('rbac', 'style_script', 'rbac.css')
        css = open(css_path, 'r', encoding='utf-8').read()
        return mark_safe(css)
    
    
    @register.simple_tag
    def rbac_js():
        """
        rabc要用到的js文件路径,并读取返回
        :return: 
        """
        js_path = os.path.join('rbac', 'style_script', 'rbac.js')
        js = open(js_path, 'r', encoding='utf-8').read()
        return mark_safe(js)
    

    新建style_script文件存放所用的js、css文件:
    rbac.css

    .rbac-hide {
        display: none;
    }
    .rbac-menu-body a {
        display: block;
    }
    
    rbac.js
    
    $('.rbac-menu-header').click(function () {
        $('.rbac-menu-body').toggleClass('rbac-hide');
        //$(this).next().removeClass('rbac-hide').parent().siblings().find('.rbac-menu-body').addClass('rbac-hide');
    });
    

    这样,菜单显示就完成了。用户登录后,假如访问index.html页面,那么只要在该模板中调用上面的自定义标签即可:

    {% load custom_tag %}
    {% load static %}
    {% load staticfiles %}
    
    
        
        
        
        
        Title
        
        
        
    {# 通过调用自定义标签中的函数,导入rbac中的css和js #}
        
        
        
    
    
    
    
    
    //your code
      {% rbac_menu request %}
    //your code //your code

    权限的后台管理

    权限的后台管理,就是提供对Model中定义的那几张表的增删改查功能。

    路由分发
    因为权限管理作为一个单独的模块,所以需要在项目的全局urls.py中作一个路由分发:
    
    from django.contrib import admin
    from django.conf.urls import url, include
    from django.urls import path
    from app01 import appviews
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        url(r'^rbac/', include('rbac.urls')),
        url(r'^login/$', appviews.login),
        url(r'^index/$', appviews.index),
        url(r'^rbac/', include('rbac.urls'))
    ]
    
    在rbac应用中新建urls.py文件定义具体的路由:
    
    from django.conf.urls import url
    from . import views
    
    urlpatterns = [
        url(r'^users/$', views.users),
        url(r'^users/new/$', views.users_new),
        url(r'^users/edit/(?P\d+)/$', views.users_edit),
        url(r'^users/delete/(?P\d+)/$', views.users_delete),
    
        url(r'^roles/$', views.roles),
        url(r'^roles/new/$', views.roles_new),
        url(r'^roles/edit/(?P\d+)/$', views.roles_edit),
        url(r'^roles/delete/(?P\d+)/$', views.roles_delete),
    
        url(r'^permissions/$', views.permissions),
        url(r'^permissions/new/$', views.permissions_new),
        url(r'^permissions/edit/(?P\d+)/$', views.permissions_edit),
        url(r'^permissions/delete/(?P\d+)/$', views.permissions_delete),
    
        url(r'^menus/$', views.menus),
        url(r'^menus/new/$', views.menus_new),
        url(r'^menus/edit/(?P\d+)/$', views.menus_edit),
        url(r'^menus/delete/(?P\d+)/$', views.menus_delete),
    
        url(r'^$', views.index)
    
    视图中处理增删改查

    定义ModelForm,首先在rbac应用新增forms.py文件,定义UserInfo的ModelForm:

    from django.forms import ModelForm
    from .models import UserInfo, Role, Permission, Menu
    
    
    class UserInfoModelForm(ModelForm):
        class Meta:
            model = UserInfo
            fields = '__all__'
            labels = {
                'username': '用户名',
                'password': '密码',
                'nickname': '昵称',
                'email': '邮箱',
                'roles': '角色',
            }
    
    
    class RoleModelForm(ModelForm):
        class Meta:
            model = Role
            fields = '__all__'
            labels = {
                'title': '角色',
                'permissions': '权限',
            }
    
    
    class PermissionModelForm(ModelForm):
        class Meta:
            model = Permission
            fields = '__all__'
            labels = {
                'title': '权限',
                'url': 'url',
                'menu': '所属菜单'
            }
    
    
    class MenuModelForm(ModelForm):
        class Meta:
            model = Menu
            fields = '__all__'
            labels = {
                'title': '菜单',
                'parent': '父级菜单',
            }
    

    视图逻辑

    from django.shortcuts import render, redirect, reverse
    from .forms import UserInfoModelForm, RoleModelForm, PermissionModelForm, MenuModelForm
    from rbac.models import UserInfo,Permission,Menu,Role
    
    # Create your views here.
    
    def index(request):
        return render(request, 'rbac/index.html')
    
    
    def users(request):
        """查询所有用户信息"""
        user_list = UserInfo.objects.all()
        return render(request, 'rbac/users.html', {'user_list': user_list})
    
    
    def users_new(request):
        if request.method =="GET":
            # 传入ModelForm对象
            model_form = UserInfoModelForm()
            return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '新增用户'})
        else:
            model_form = UserInfoModelForm(request.POST)
            if model_form.is_valid():
                model_form.save()
                return redirect(reverse(users))
            else:
                return render(request, 'rbac/common_edit.html',{'model_form': model_form, 'title': '新增用户'})
    
    
    def users_edit(request,id):
        user_obj = UserInfo.objects.filter(id=id).first()
        if request.method == 'GET':
            model_form = UserInfoModelForm(instance=user_obj)
            return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '编辑用户'})
        else:
            model_form = UserInfoModelForm(request.POST, instance=user_obj)
            if model_form.is_valid():
                model_form.save()
                return redirect(reverse(users))
            else:
                return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '编辑用户'})
    
    
    def users_delete(request, id):
        user_obj = UserInfo.objects.filter(id=id).first()
        user_obj.delete()
        return redirect(reverse(users))
    
    
    def roles(request):
        role_list = Role.objects.all()
        return render(request, 'rbac/roles.html', {'role_list': role_list})
    
    
    def roles_new(request):
        if request.method == "GET":
            # 传入ModelForm对象
            model_form = RoleModelForm()
            return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '新增角色'})
        else:
            model_form = RoleModelForm(request.POST)
            if model_form.is_valid():
                model_form.save()
                return redirect(reverse(roles))
            else:
                return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '新增角色'})
    
    
    def roles_edit(request, id):
        role_obj = Role.objects.filter(id=id).first()
        if request.method == 'GET':
            model_form = RoleModelForm(instance=role_obj)
            return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '编辑角色'})
        else:
            model_form = RoleModelForm(request.POST, instance=role_obj)
            if model_form.is_valid():
                model_form.save()
                return redirect(reverse(roles))
            else:
                return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '编辑角色'})
    
    
    def roles_delete(request, id):
        role_obj = Role.objects.filter(id=id).first()
        role_obj.delete()
        return redirect(reverse(roles))
    
    
    def permissions(request):
        permission_list = Permission.objects.all()
        return render(request, 'rbac/permissions.html', {'permission_list': permission_list})
    
    
    def permissions_new(request):
        if request.method == "GET":
            # 传入ModelForm对象
            model_form = PermissionModelForm()
            return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '新增权限'})
        else:
            model_form = PermissionModelForm(request.POST)
            if model_form.is_valid():
                model_form.save()
                return redirect(reverse(permissions))
            else:
                return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '新增权限'})
    
    
    def permissions_edit(request, id):
        permission_obj = Permission.objects.filter(id=id).first()
        if request.method == 'GET':
            model_form = PermissionModelForm(instance=permission_obj)
            return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '编辑权限'})
        else:
            model_form = PermissionModelForm(request.POST, instance=permission_obj)
            if model_form.is_valid():
                model_form.save()
                return redirect(reverse(permissions))
            else:
                return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '编辑权限'})
    
    
    def permissions_delete(request, id):
        permission_obj = Role.objects.filter(id=id).first()
        permission_obj.delete()
        return redirect(reverse(permissions))
    
    
    def menus(request):
        menu_list = Menu.objects.all()
        return render(request, 'rbac/menus.html', {'menu_list': menu_list})
    
    
    def menus_new(request):
        if request.method == "GET":
            # 传入ModelForm对象
            model_form = MenuModelForm()
            return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '新增菜单'})
        else:
            model_form = MenuModelForm(request.POST)
            if model_form.is_valid():
                model_form.save()
                return redirect(reverse(menus))
            else:
                return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '新增菜单'})
    
    
    
    def menus_edit(request, id):
        menu_obj = Menu.objects.filter(id=id).first()
        if request.method == 'GET':
            model_form = MenuModelForm(instance=menu_obj)
            return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '编辑菜单'})
        else:
            model_form = MenuModelForm(request.POST, instance=menu_obj)
            if model_form.is_valid():
                model_form.save()
                return redirect(reverse(menus))
            else:
                return render(request, 'rbac/common_edit.html', {'model_form': model_form, 'title': '编辑菜单'})
    
    
    def menus_delete(request, id):
        menu_obj = Role.objects.filter(id=id).first()
        menu_obj.delete()
        return redirect(reverse(menus))
    

    你可能感兴趣的:(Django权限管理实现)