news 2026/9/12 14:09:58

Django开箱即用的RBAC权限系统:从模型到菜单的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django开箱即用的RBAC权限系统:从模型到菜单的完整实现

简介:基于Django的开箱即用RBAC(基于角色的权限管理)系统,面向Web开发初学者、相关专业在校生以及需要快速搭建权限模块的开发者,可有效解决角色、用户、权限三者间的授权与校验落地问题。资源共34个文件,以29个Python源码为主体,覆盖settings配置、models模型、views视图、serializers序列化、admin后台等Django标准分层,另含Pipfile依赖清单、README说明及gitignore等文件,整体压缩包仅26KB,结构清晰、便于通读和二次修改。已有45人学习下载。内容包括完整的RBAC核心逻辑、自定义响应封装、权限校验与测试用例,并附有简要文档说明,既能用于课程设计、毕业设计快速演示,也适合在此基础上扩展业务模块、积累Django项目开发经验。

1. 从重复造轮子到开箱即用的Django RBAC系统

一个企业内部管理系统发展了几个月,权限判断散落在视图和模板里,今天加一个“是否是主管”的if,明天改一个“只能看本部门数据”的过滤。真正把权限做成一个模块时,光是把用户、角色、权限之间的关系理清就要重写一遍。题目里的“开箱即用的RBAC”就是面对这些问题:预置好用户角色关联、权限校验、菜单生成和部署文档,让新项目或老系统改造时不用再做选择题。它会覆盖Django从模型到视图、模板的全链路,适合后端开发、架构师和要交付毕业设计的学生。下面我按自己整理这种资料包的方式,把表结构、权限校验、动态菜单和文档组织逐层拆开,直接讲可落地的方案。

2. 设计RBAC核心模型:Django中的用户、角色、权限表

2.1 为什么直接用Django自带Permission不够

Django自带的auth系统提供了User、Group和Permission,Group本身就承担了“角色”的角色。对于极简单的后台,用admin后台把用户加入Group,再给Group分配权限,确实能跑通。但在真实业务里,Group无法表达角色编码、角色层级和数据范围;连菜单权限绑定也得另外再建表。开箱即用的RBAC通常保留Django的Permission作为操作权限的载体,另外新增Role表和Menu表,让User与Role建立多对多,Role与Permission建立多对多。这样权限模型既能复用Django自带的has_perm,又能满足扩展需求。

为什么不直接改Group?因为Group的name只适合展示,不适合做稳定标识;同时我们还要对菜单做权限过滤,需要外键关联。因此自建Role是一个低成本高可维护的做法。

2.2 核心模型代码:User、Role、Menu

在实际项目中,我一般把用户模块放在单独的accounts应用中。先创建应用,再定义模型。

python manage.py startapp accounts

models.py的核心如下:

from django.contrib.auth.models import AbstractUser, Permission from django.db import models class User(AbstractUser): roles = models.ManyToManyField('Role', related_name='users', blank=True) class Meta: verbose_name = '用户' verbose_name_plural = verbose_name class Role(models.Model): name = models.CharField('角色名称', max_length=64, unique=True) code = models.CharField('角色编码', max_length=64, unique=True) permissions = models.ManyToManyField( Permission, verbose_name='权限集合', related_name='roles', blank=True, ) def __str__(self): return self.name class Menu(models.Model): title = models.CharField('菜单标题', max_length=64) name = models.CharField('前端路由名', max_length=64, blank=True) path = models.CharField('前端路径', max_length=255, blank=True) icon = models.CharField('图标', max_length=64, blank=True) parent = models.ForeignKey( 'self', verbose_name='父菜单', null=True, blank=True, on_delete=models.CASCADE, ) order = models.IntegerField('排序', default=0) permission = models.ForeignKey( Permission, verbose_name='关联权限', null=True, blank=True, on_delete=models.SET_NULL, ) class Meta: ordering = ['order'] def __str__(self): return self.title

这段代码的逻辑说明:User不直接持有权限,而是通过roles进入Role表,再由Role.permissions拿到permission,形成“用户-角色-权限”的三层模型,比把权限直接挂在User上更容易维护。Role.code是稳定标识,比如admin、operator,代码判断角色时用code而不是name。Menu.permission是可空的,顶级菜单不需要权限外键,二级菜单的显示则由permission控制;权限与菜单一对一时逻辑最清晰,如果一个页面有查看和导出两个权限,可以再建一个菜单按钮表,但大部分后台系统用一对多就够用。

模型主要字段作用
Userroles M2M关联角色,获取权限入口
Rolename / code / permissions M2M角色分组与权限集合
Menupath / parent / order / permission FK生成动态菜单与按钮控制

2.3 数据迁移与初始化超级用户

定义完模型后,依次执行迁移:

python manage.py makemigrations accounts python manage.py migrate python manage.py createsuperuser

因为User替换了默认用户,createsuperuser创建的是accounts.User。之后进入admin后台,在Role表里创建“管理员”和“普通用户”,给管理员勾选全部权限。权限数据来自Django在migrate时自动填充的Permission记录,不需要手动插入。菜单表通常通过fixture或后台录入,如果是一次性初始化,也可以在迁移后用python manage.py shell执行一段脚本,创建菜单并绑定Permission,而不是直接在数据库手写外键值。

这里有一个容易被忽略的细节:Django迁移默认生成的权限codename是“add_user”“change_user”这种固定格式,app_label为“accounts”。要删除某个权限对应的关联对象,不要直接操作数据库,应该通过模型删除,比如Role.objects.get(code='operator').delete(),否则缓存中的权限列表会残留;这类“Django执行查询-删除对象”的经验在权限资料包里会作为排错建议写进FAQ。

3. 把权限变成可执行的校验:装饰器、中间件与模板

3.1 权限校验装饰器:让每个视图都拿到同一份保护

Django本地有django.contrib.auth.decorators.permission_required,但默认行为是所有非超管都跳转登录页,对API不够友好。开箱即用的RBAC资料包里,我一般提供一层薄封装:

# accounts/decorators.py from functools import wraps from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied def require_perm(perm, login_url=None, raise_exception=True): def decorator(view_func): @wraps(view_func) def _wrapped_view(request, *args, **kwargs): if not request.user.is_authenticated: return login_required(view_func, login_url=login_url)(request, *args, **kwargs) if request.user.is_superuser or request.user.has_perm(perm): return view_func(request, *args, **kwargs) if raise_exception: raise PermissionDenied('没有执行该操作的权限') from django.shortcuts import redirect return redirect(login_url or 'login') return _wrapped_view return decorator

代码逻辑说明:先判断是否登录,未登录的走login_required;已登录且是超管直接放行;其余用户用has_perm(perm)校验权限。perm必须是“app_label.codename”格式,例如order.view_orderraise_exception默认True,对非前后端分离的页面也可以设为False,让它重定向到登录页或403页。这样视图层可以写:

@require_perm('order.view_order') def order_list(request): ...

比在函数内部写if not request.user.has_perm(...)更直观,也避免在N个视图里重复判断。

3.2 自定义中间件做URL级权限过滤:免装饰器的备选方案

装饰器只能保护函数或类视图,如果使用的是第三方库视图、Django Admin或者已经写完的旧接口,不便于逐个加装饰器时,可以用中间件按URL统一过滤。先配置URL与权限码的映射表,再在中间件中比对。

# config/url_permissions.py URL_PERMISSION_MAP = { '/order/list/': 'order.view_order', '/order/add/': 'order.add_order', '/api/v1/order/': 'order.view_order', } # accounts/middleware.py from django.http import HttpResponseForbidden from django.shortcuts import redirect from config.url_permissions import URL_PERMISSION_MAP class RBACUrlPermissionMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): permission = URL_PERMISSION_MAP.get(request.path_info) if permission: user = request.user if not user.is_authenticated: return redirect('login') if not (user.is_superuser or user.has_perm(permission)): return HttpResponseForbidden('<h1>403 Forbidden</h1>', content_type='text/html') return self.get_response(request)

这个方案的代价是维护URL映射表,路径带参数时要使用正则匹配,而不只是dict.get。常见做法是把映射表改成列表,元素是(r'^/order/(?P<pk>\d+)/edit/$', 'order.change_order'),中间件里做逐一匹配。它不太适合页面URL天天变的项目,所以装饰器和中间件可以并存:装饰器用于新增接口,中间件只兜底旧路由。三种校验方案的选型如下表:

方案粒度适用场景维护成本
require_perm装饰器视图级后端接口、新开发页面
RBACUrlPermissionMiddlewareURL级老系统改造、第三方视图中,需维护URL映射
模板标签/过滤器模板级控制按钮和菜单显示

3.3 模板中控制按钮显示:一个过滤器就能搞定

很多后台页面权限校验通过后,页面里的“新增”“删除”按钮还要按权限决定是否渲染。Django权限自带perms模板变量,可以直接写{% if perms.order.add_order %}。如果想在复杂条件里复用,可以封装成模板过滤器:

# accounts/templatetags/perm_tags.py from django import template register = template.Library() @register.filter def can(user, perm): if not user or not user.is_active: return False return user.is_superuser or user.has_perm(perm)

模板中使用:

{% load perm_tags %} {% if request.user|can:"order.delete_order" %} <button class="btn btn-danger">删除订单</button> {% endif %}

注意request.user | can需要传入的是用户对象,而不是perms变量。原因是perms本身只包含当前用户的权限集合,用过滤器可以额外做超管直通判断并统一一层逻辑;模板里因此少写一些嵌套if。模板标签只做显示层控制,不能替代后端校验,这是RBAC安全边界的基本原则。

4. RBAC动态菜单与接口输出:给前端一个开箱即用的后端

4.1 菜单权限绑定的两种设计

后端给前端返回菜单时,常见有两种做法。一种是把所有菜单一次性拉到前端,前端根据用户拥有的权限码过滤路由;另一种是后端在返回菜单接口时,就直接只查有权限的菜单。推荐后者,因为前端不持有完整菜单结构,减少暴露无关信息,也更符合“后端控制权限”的原则。由于我们的Menu表里已经存了permission外键,实现起来就是根据用户权限过滤菜单集合,再组成树。

设计方式前端工作量后端工作量安全性
前端持有全部菜单,按权限码过滤中,需要维护路由meta只需返回权限码后端菜单结构可能被泄露
后端只返回有权限的菜单树低,直接注册路由需要构建树后端完全控制菜单可见性

4.2 构建菜单树的工具函数

编写一个通用函数,输入user,输出菜单树JSON:

def build_menu_tree(user): if user.is_superuser: menus = Menu.objects.all() else: role_perms = Permission.objects.filter(role__in=user.roles.all()).distinct() menus = Menu.objects.filter(permission__in=role_perms) free_menus = Menu.objects.filter(permission=None) menus = (menus | free_menus).distinct() menu_list = list(menus) node_map = {menu.id: { 'id': menu.id, 'title': menu.title, 'name': menu.name, 'path': menu.path, 'icon': menu.icon, 'children': [], } for menu in menu_list} roots = [] for menu in menu_list: node = node_map[menu.id] if menu.parent_id and menu.parent_id in node_map: node_map[menu.parent_id]['children'].append(node) else: roots.append(node) return sorted(roots, key=lambda x: x['order']) if roots else roots

逻辑说明:先查出当前用户可见的菜单列表,再按parent_id组装成树。过滤查询时,必须把permission=None的免费菜单合并进来,否则顶级菜单会全部消失。sorted对roots排序,子菜单的顺序依赖Menu.Meta.ordering,因此构建时不需要额外处理。如果一个菜单没有关联permission,说明所有登录用户可见。这里需要注意,distinct()后不能再沿用原有排序字段,所以排序放到Python侧做,代码更稳。

4.3 给Vue前端返回路由与权限码

在基于Django + Vue的前后端分离项目里,菜单接口通常长这样:

{ "code": 0, "data": { "menus": [...], "perms": ["order.view_order", "order.add_order"] } }

后端视图可以这样写:

from django.http import JsonResponse def user_routes(request): user = request.user if not user.is_authenticated: return JsonResponse({'code': 401, 'msg': '未登录'}) return JsonResponse({ 'code': 0, 'data': { 'menus': build_menu_tree(user), 'perms': list(user.roles.values_list('permissions__codename', flat=True).distinct()), } })

前端拿到menus后,通过router.addRoute动态注册路由,再在路由守卫里用router.hasRoute(record.name)route.meta.roles做拦截。开箱即用的资料包里,后端这部分一般只会提供接口契约和示例,具体前端框架差异太大,不建议把Vue项目一起塞进zip,除非整套系统是“可运行demo”而不是“可集成模块”。

这里也解释了一个高频问题:为什么菜单接口里要同时返回perms?因为前端按钮级权限控制需要权限码,如果只返回菜单,页面里的“导出”“删除”按钮还是不知道要不要显示。所以完整RBAC的返回数据里,菜单是给路由用的,perms是给按钮判断用的,两者不要混在一起。

5. 资料包里的“详细文档”怎么组织:部署、排错与后台体验

5.1 资料包目录结构与README

“全部资料+详细文档”的zip,如果只是打包源码,使用者装完依赖仍然不知道先跑哪条命令。我一般会在zip里放一个README.md和docs目录,结构大致是:

路径说明
README.md环境要求、快速启动、默认账号
docs/deploy.md宝塔部署Django步骤与nginx配置
docs/auth.mdRBAC模型说明、权限码命名规范
docs/api.md动态菜单、登录、权限校验的接口文档
rbac_demo/Django项目源码
requirements.txtPython依赖
init_data.json初始角色与菜单数据

README开头就写三件事:Python版本要求(比如Python 3.8+)、数据库选择(本地SQLite或MySQL)、启动命令。很多用户打开zip会先找“资料包.txt”,不如直接给一份可以直接跑起来的命令清单:

# 创建虚拟环境 python3 -m venv venv source venv/bin/activate # 安装依赖 pip install -r requirements.txt # 初始化数据库 python manage.py makemigrations python manage.py migrate python manage.py loaddata init_data.json # 启动开发服务器 python manage.py runserver 0.0.0.0:8000

这份命令同样适用于Django教程中常见的本地开发场景。loaddata init_data.json是开箱即用的关键,fixture里包含初始的角色、菜单和权限数据,让项目第一次启动就能看到一个能跑的后台,而不是空白数据库。

5.2 宝塔部署Django与mysqlclient安装的注意事项

我接手过的部署环境里,宝塔面板是最常见的Linux图形化管理方式。宝塔部署Django时,坑通常集中在Python环境和MySQL驱动。如果你用MySQL而不是SQLite,执行pip install -r requirements.txt时常遇到的报错是:

ERROR: Command errored out with exit status 1: ... mysqlclient cannot be compiled

原因是你没有安装MySQL开发头文件。Debian/Ubuntu上执行:

apt install python3-dev default-libmysqlclient-dev build-essential

CentOS/宝塔Linux面板则执行:

yum install python3-devel mysql-devel gcc gcc-c++

安装完成后再pip install mysqlclient即可。宝塔面板中部署Django,一般会配置一个Python项目站点,选择项目的启动文件为wsgi.py,设置好Python解释器后,再添加nginx反向代理。静态文件还需要执行python manage.py collectstatic,然后让nginx直接指向静态目录。以下是一段常见的nginx配置片段,放在宝塔站点配置的location /中:

location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }

这里需要解释的是,proxy_set_header不能省略,否则Django的request.build_absolute_uri()会得到错误协议或域名,导致登录后重定向地址错误。另外,因为权限后台需要登录,session和CSRF的COOKIE没有设置secure时,用HTTP访问没问题;一旦上线HTTPS,记得在settings.py中加上CSRF_COOKIE_SECURE = TrueSESSION_COOKIE_SECURE = True,否则会被浏览器直接丢弃。

5.3 把Django Admin做成权限管理后台

标题里的“开箱即用”还应该包含一个可以给非技术人员使用的权限管理页面。Django Admin天然适合做这件事,只需要在admin.py中注册Role和Menu,配置好字段:

# accounts/admin.py from django.contrib import admin from .models import User, Role, Menu @admin.register(Role) class RoleAdmin(admin.ModelAdmin): list_display = ('name', 'code') filter_horizontal = ('permissions',) @admin.register(Menu) class MenuAdmin(admin.ModelAdmin): list_display = ('title', 'parent', 'order', 'permission') list_filter = ('parent',)

使用filter_horizontal后,给角色分配权限时左侧是可选权限,右侧是已选权限,比默认的多选框体验好很多。Django Admin界面美化不是必须项,但开箱即用的后台可以顺带加上admin.site.site_header = 'RBAC权限后台',让标题栏不再是默认的“Django administration”。这套后台只做管理用途,面向端用户的业务页面仍通过前面的装饰器和菜单接口控制权限。

6. RBAC权限系统的三个进阶技巧:权限码命名、缓存与超管设计

6.1 权限码命名规范

如果权限全部依赖Django自动生成的add/change/delete,页面按钮权限和接口权限会相互混淆。我建议把自定义权限写进Model的Meta里:

# order/models.py class Order(models.Model): class Meta: default_permissions = ('add', 'change', 'delete', 'view') permissions = ( ('export_order', '导出订单'), ('approve_order', '审批订单'), )

这样权限码会生成order.export_orderorder.approve_order。在权限资料包中,需要统一约定“应用小写.动词_模型小写”的格式,动词优先使用view/add/change/delete/export/import,后续做数据权限、操作审计时,可以直接通过权限码字符串分类,不需要额外维护表。

6.2 缓存用户权限集

角色权限数量多时,每次has_perm都查一次数据库,几百个用户同时访问后台就很明显。开箱即用方案里一般增加一层缓存:

from django.core.cache import cache def get_user_permissions(user): key = f'user_perms_{user.id}' perms = cache.get(key) if perms is None: perms = list( Permission.objects.filter(role__in=user.roles.all()) .values_list('content_type__app_label', 'codename') ) cache.set(key, perms, 60 * 10) return [f'{app}.{codename}' for app, codename in perms]

这里的app_label是Permission所在应用名,不能用角色名代替。缓存10分钟已经足够,权限变更后通过Role的post_save信号删除关联用户的缓存,这样一个角色权限修改后,不必等10分钟就能重新生效。

6.3 超级用户绕过校验的统一入口

开箱即用的系统一定会遇到“为什么我是超级用户还是403”的问题。原因是装饰器、中间件、模板标签各写了一次is_superuser判断,漏掉一处就会出问题。最稳妥的方式是把判断抽成公共函数:

def has_perm_or_super(user, perm): return user.is_superuser or user.has_perm(perm)

然后在装饰器、中间件、模板过滤器全部改用它。即使某个视图忘了加装饰器,只要中间件覆盖了该路径,权限依然有效;反之中间件没覆盖,装饰器也能兜住。最后的实践建议是,把has_perm_or_super放进accounts/utils.py,并在代码评审时只允许它作为权限入口,而不是散落的多层if。RBAC从“能跑”到“能交付”,差别往往就在这些统一出口和缓存细节上。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 14:09:23

避开桌面软件,3款Web端开源ER图工具实测:选型与实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 14:08:40

LunaTranslator 使用指南:把日文游戏实时翻译成中文的完整步骤

LunaTranslator 使用指南&#xff1a;把日文游戏实时翻译成中文的完整步骤 【免费下载链接】LunaTranslator 视觉小说翻译器 / Visual Novel Translator 项目地址: https://gitcode.com/GitHub_Trending/lu/LunaTranslator LunaTranslator 是一款免费的视觉小说翻译工具…

作者头像 李华
网站建设 2026/9/12 14:07:37

aarch64嵌入式Qt静态交叉编译部署手册

做aarch64嵌入式开发这几年&#xff0c;我见过的Qt部署翻车现场不算少。最早在RK3399板子上调一个Qt应用&#xff0c;程序编出来了&#xff0c;目标板上缺libQt5Core.so.5&#xff0c;用NFS挂载把宿主机的库共享过去&#xff0c;结果板子glibc偏老&#xff0c;库一加载直接段错…

作者头像 李华
网站建设 2026/9/12 14:05:19

微服务可观测性:分布式系统调试的核心技术

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 14:04:10

dcode 执行 /offload 时返回 409 怎么排查?

dcode 执行 /offload 时返回 409 怎么排查&#xff1f; 【免费下载链接】deepagents The batteries-included agent harness. 项目地址: https://gitcode.com/GitHub_Trending/de/deepagents 在 dcode&#xff08;deepagents-code&#xff09;的 TUI 会话里执行 /offloa…

作者头像 李华
网站建设 2026/9/12 14:03:33

PPSSPP 作弊教程:3 步启用 CwCheat,一次搞懂 PSP 模拟器作弊码

PPSSPP 作弊教程&#xff1a;3 步启用 CwCheat&#xff0c;一次搞懂 PSP 模拟器作弊码 【免费下载链接】ppsspp A PSP emulator for Android, Windows, Mac, Linux and iOS, written in C. Want to contribute? Join us on Discord at https://discord.gg/5NJB6dD or just sen…

作者头像 李华