拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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和PermissionGroup本身就承担了“角色”的角色。对于极简单的后台用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 accountsmodels.py的核心如下from django.contrib.auth.models import AbstractUser, Permission from django.db import models class User(AbstractUser): roles models.ManyToManyField(Role, related_nameusers, blankTrue) class Meta: verbose_name 用户 verbose_name_plural verbose_name class Role(models.Model): name models.CharField(角色名称, max_length64, uniqueTrue) code models.CharField(角色编码, max_length64, uniqueTrue) permissions models.ManyToManyField( Permission, verbose_name权限集合, related_nameroles, blankTrue, ) def __str__(self): return self.name class Menu(models.Model): title models.CharField(菜单标题, max_length64) name models.CharField(前端路由名, max_length64, blankTrue) path models.CharField(前端路径, max_length255, blankTrue) icon models.CharField(图标, max_length64, blankTrue) parent models.ForeignKey( self, verbose_name父菜单, nullTrue, blankTrue, on_deletemodels.CASCADE, ) order models.IntegerField(排序, default0) permission models.ForeignKey( Permission, verbose_name关联权限, nullTrue, blankTrue, on_deletemodels.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(codeoperator).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_urlNone, raise_exceptionTrue): 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_urllogin_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_order。raise_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(h1403 Forbidden/h1, content_typetext/html) return self.get_response(request)这个方案的代价是维护URL映射表路径带参数时要使用正则匹配而不只是dict.get。常见做法是把映射表改成列表元素是(r^/order/(?Ppk\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 classbtn btn-danger删除订单/button {% endif %}注意request.user | can需要传入的是用户对象而不是perms变量。原因是perms本身只包含当前用户的权限集合用过滤器可以额外做超管直通判断并统一一层逻辑模板里因此少写一些嵌套if。模板标签只做显示层控制不能替代后端校验这是RBAC安全边界的基本原则。4. RBAC动态菜单与接口输出给前端一个开箱即用的后端4.1 菜单权限绑定的两种设计后端给前端返回菜单时常见有两种做法。一种是把所有菜单一次性拉到前端前端根据用户拥有的权限码过滤路由另一种是后端在返回菜单接口时就直接只查有权限的菜单。推荐后者因为前端不持有完整菜单结构减少暴露无关信息也更符合“后端控制权限”的原则。由于我们的Menu表里已经存了permission外键实现起来就是根据用户权限过滤菜单集合再组成树。设计方式前端工作量后端工作量安全性前端持有全部菜单按权限码过滤中需要维护路由meta只需返回权限码后端菜单结构可能被泄露后端只返回有权限的菜单树低直接注册路由需要构建树后端完全控制菜单可见性4.2 构建菜单树的工具函数编写一个通用函数输入user输出菜单树JSONdef build_menu_tree(user): if user.is_superuser: menus Menu.objects.all() else: role_perms Permission.objects.filter(role__inuser.roles.all()).distinct() menus Menu.objects.filter(permission__inrole_perms) free_menus Menu.objects.filter(permissionNone) 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, keylambda x: x[order]) if roots else roots逻辑说明先查出当前用户可见的菜单列表再按parent_id组装成树。过滤查询时必须把permissionNone的免费菜单合并进来否则顶级菜单会全部消失。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, flatTrue).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-essentialCentOS/宝塔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 True和SESSION_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_order和order.approve_order。在权限资料包中需要统一约定“应用小写.动词_模型小写”的格式动词优先使用view/add/change/delete/export/import后续做数据权限、操作审计时可以直接通过权限码字符串分类不需要额外维护表。6.2 缓存用户权限集角色权限数量多时每次has_perm都查一次数据库几百个用户同时访问后台就很明显。开箱即用方案里一般增加一层缓存from django.core.cache import cache def get_user_permissions(user): key fuser_perms_{user.id} perms cache.get(key) if perms is None: perms list( Permission.objects.filter(role__inuser.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从“能跑”到“能交付”差别往往就在这些统一出口和缓存细节上。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门