Django+Vue.js RBAC权限管理系统:从模型设计到前后端联动
简介面向计算机、软件工程专业的毕业设计需求这是一套基于Django与Vue.js、采用前后端分离架构的RBAC权限管理系统完整项目提供源码与论文适合需要权限控制方案的开发者。系统覆盖用户、角色、权限、数据权限、操作日志等管理模块支持细粒度动态授权并融合Celery异步处理、RESTful API、MVC及模块化设计。压缩包共329个文件以282个py后端源码、JSON配置、Markdown文档、Shell脚本、HTML模板等为主另有部署配置与论文文档整体仅1.02MB。目前已有111人浏览学习。借助完整源码、论文、部署指南和测试用例可快速搭建运行环境深入理解RBAC权限模型、前后端交互与项目工程化组织方式适合作为毕业设计参考或实际项目基础进行扩展。1. 从“看似简单”到“到处踩坑”的RBAC后台权限做过几个后台管理系统之后我越来越确认一件事权限管理才是后台系统的隐形工期杀手。很多开发者一开始觉得不就是“登录、菜单、按钮显隐”吗结果越往后做越发现权限跑到一半就开始失控——某个接口没校验、某个角色多了一截越权路径、前端菜单和后端接口权限对不上。尤其是用 Djang o 做后端、Vue.js 做前端的组合框架本身给了一部分权限能力但离一个真正能上线的 RBAC 权限管理系统还差着“最后一公里”。这个标题对应的东西说白了就是一套用 Django 提供鉴权接口和数据模型、用 Vue.js 做动态菜单与路由拦截、以 RBAC基于角色的访问控制为核心模型的管理系统完整实现。它的价值在于你不需要从零去研究权限该怎么做而是拿到一套把角色、用户、权限绑定关系落到数据库再反映到前端界面的工程化方案。适合谁做毕业设计的在校生、要快速搭建内部后台的中小团队、以及把权限模块反复写过很多次但想规范化沉淀的开发者。但我要先泼一盆冷水这也是很多网上下载的源码最容易翻车的地方代码能跑通权限却经不起推敲。菜单显示了、接口没拦住或者角色改了权限、前端菜单不刷新这种“半吊子权限系统”太多了。这篇文章我不打算给你贴一份能直接运行的完整代码而是把一套可复现的设计思路、核心实现片段和踩坑记录拆开讲透。你跟着搭一遍再看网上那些源码一眼就能看出它的权限设计到底是真有料还是只是摆设。2. 为什么是 Django Vue.jsRBAC 的实现分工与选型边界2.1 Django 侧auth 框架离 RBAC 还差什么Django 自带一个 auth 系统里面有 User、Group、Permission 三张核心表也提供了has_perm()这类校验方法。初次接触的人很容易误以为这就是 RBAC 了。实际用下来你会发现Django 原生权限的粒度是“模型级”的——它能控制某个用户能不能对某个模型执行增删改查但控制不到“某条订单里的某个字段”。而且权限的分配方式偏底层你要给一个角色配权限得手动去 Permission 表里找记录再来关联非常不直观。所以常见做法是保留 Django auth 的表结构作为底层底座但把它的 Permission 关联关系重新组织或者引入自己定义的角色表和用户—角色、角色—权限中间表。我在实际项目里通常选择自建Role模型然后用 ManyToMany 关联这样角色的增删、权限勾选都可以做成可视化界面而不是直接操作 Django Admin。这里要注意一个边界如果你的系统角色固定就两三种、权限只有菜单级Django 原生 auth 甚至直接配 is_staff / is_superuser 就够了没必要上完整 RBAC。但标题既然叫“RBAC 权限管理系统”就必须把角色、权限、用户三者的动态关系做成数据驱动的而不是写死在代码里。2.2 Vue.js 侧前端权限控制的真正边界前端在权限系统里扮演的角色说到底是“体验层”而非“安全层”。Vue Router 的全局前置守卫、动态路由注册、菜单根据角色过滤渲染按钮这些都是前端权限控制的常规手段。但有一条铁律必须刻在脑子里前端隐藏了某个按钮接口照样能被人用 Postman 直接调用。真正拦住越权的永远是后端接口的鉴权。那前端的权限控制还有没有必要做当然有。一个销售登录进去看到财务的导出按钮即使接口能拦他这个界面本身就在泄露信息结构而按钮级的权限控制能让界面的功能呈现和职位匹配减少误操作。所以正确的分工是前端负责“展示什么、能点到什么”后端负责“能执行什么、能拿到什么数据”两者各管一段缺一不可。前端实现上经常用到的套路是登录后从后端拿到当前用户的路由表或权限标识列表存到全局状态里用router.addRoutes()动态注册路由同时根据权限标识控制菜单和按钮的v-if显隐。这也就引出了整篇文章最核心的一段联动逻辑后端给什么数据前端就渲染什么界面。2.3 权限数据流一次点击背后的完整链路我习惯把权限数据流画成一条清晰的责任链这样前后端配合写起来才不会乱套。用户发起登录请求Django 校验账号密码后签发一个 Token同时返回这个用户对应的权限标识集合。前端收到后把它存进 Pinia或 Vuex然后路由守卫里做判断如果当前访问的路由需要特定权限标识而这个用户没有就重定向到 403 页面。用户点击某个操作按钮时前端先用指令或自定义校验函数确认是否有权显示点击后请求到达 Django 接口Django 在视图层先做认证再查权限最后才执行业务逻辑。任何一环漏了权限系统就会出现越权漏洞或者体验割裂。后面两章我会把这条链路的两边分别展开先写后端的数据模型与接口鉴权再写前端的动态路由与按钮级控制。3. 后端落地从数据模型到接口鉴权的完整实现3.1 数据模型设计用户、角色、权限三张核心表第一步永远是建模。我建议不要直接拿 Django 的 Group 当角色表因为后面你要做角色管理的前端界面Group 的字段太少扩展起来不舒服。自己建Role模型字段至少包含角色名、角色标识、描述、状态。权限表沿用 Django 的Permission就行它已经有codename和content_type够用了。下面是核心模型代码包含用户角色关联和角色权限关联from django.contrib.auth.models import AbstractUser, Permission from django.db import models class User(AbstractUser): # 扩展用户字段比如手机号、岗位 phone models.CharField(max_length20, blankTrue) class Meta: db_table sys_user class Role(models.Model): name models.CharField(max_length50, verbose_name角色名称) code models.CharField(max_length50, uniqueTrue, verbose_name角色标识) description models.CharField(max_length255, blankTrue) status models.BooleanField(defaultTrue, verbose_name是否启用) # 角色与权限的多对多关系 permissions models.ManyToManyField( Permission, blankTrue, related_nameroles_in_perm, verbose_name权限列表 ) class Meta: db_table sys_role # 用户与角色是多对多放在用户模型上直接声明 # 但为了后期扩展单独建一个中间模型更灵活 class UserRole(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameuser_roles) role models.ForeignKey(Role, on_deletemodels.CASCADE, related_namerole_users) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table sys_user_role unique_together (user, role)逻辑说明用户通过 UserRole 中间表和角色关联角色通过自带的 ManyToMany 和权限关联这就在数据库层面形成了“用户—角色—权限”的经典 RBAC 三角关系。related_name必须写清楚不然默认的反向查询名称会和 Django 自带的用户权限字段冲突。参数说明角色标识code加上uniqueTrue是为了在前端用字符串判断权限时不至于拿着中文名到处比较例如admin、operator、viewer。status字段用来做角色的启用与停用比直接删除角色更安全因为删除角色会级联影响历史数据。3.2 自定义权限校验装饰器与中间件的选择Django 的permission_required装饰器默认校验的是“当前用户是否拥有某个模型权限”它不会自动把你的角色权限映射进来。所以我一般直接写一个自定义装饰器专门用来校验请求头里的 Token、解析当前用户、再判断角色是否绑定了目标权限。这里有一个校验点容易踩坑角色被停用了但用户还拥有这个角色关联应该提示“无权限”而不是“认证失败”。认证是确认你是谁授权是确认你能干什么两者必须分开判断。from functools import wraps from django.http import JsonResponse from rest_framework.authtoken.models import Token def permission_required(perm_code): 自定义权限校验装饰器perm_code 传入权限标识如 user:add def decorator(view_func): wraps(view_func) def wrapper(request, *args, **kwargs): user None # 从请求头取 Token auth_header request.META.get(HTTP_AUTHORIZATION, ) if auth_header.startswith(Token ): token_key auth_header.split( )[1] try: user Token.objects.select_related(user).get(keytoken_key).user except Token.DoesNotExist: return JsonResponse({code: 401, msg: 登录已过期}, status401) if not user: return JsonResponse({code: 401, msg: 未登录}, status401) # 超级管理员直接放行 if user.is_superuser: return view_func(request, *args, **kwargs) # 查询用户所有角色的权限标识 has_perm user.user_roles.filter( role__statusTrue, role__permissions__codenameperm_code ).exists() if not has_perm: return JsonResponse({code: 403, msg: 无权限访问}, status403) return view_func(request, *args, **kwargs) return wrapper return decorator逻辑说明这个装饰器从每个请求头解析 Token再反向查到用户随后查询用户所有启用状态下角色的权限里有没有包含传入的perm_code。is_superuser直接放行是管理员体验的关键否则你给超级管理员配角色权限会配到怀疑人生。参数说明perm_code的命名建议按“模块:动作”的格式组织例如user:add、order:export。不要用 Django 默认的add_user这种格式因为前端要拿这个字符串做按钮判断带冒号的可读性和扩展性明显更好。3.3 接口鉴权落到 URL 配置视图函数与 APIView 两种方式装饰器写好后接下去就是把它挂到接口上。常见的项目会混用 Django 原生视图函数和 DRFDjango REST Framework的 APIView。两种写法我都列出来看你项目用的是哪一种。# 方式一纯 Django 视图函数 from django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse import json csrf_exempt permission_required(user:add) def add_user(request): if request.method POST: data json.loads(request.body) # 这里写创建用户的业务逻辑 return JsonResponse({code: 200, msg: 创建成功}) return JsonResponse({code: 405, msg: 请求方式不支持}, status405) # 方式二DRF 的 APIView推荐 from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status class UserListView(APIView): permission_classes [] # 关闭 DRF 自带权限让自定义装饰器统一管理 permission_required(user:list) def get(self, request): users User.objects.filter(is_activeTrue) # 序列化并返回用户列表 data [{id: u.id, username: u.username} for u in users] return Response({code: 200, data: data}, statusstatus.HTTP_200_OK)逻辑说明我推荐 DRF 的 APIView 的原因在于它和 Django 的 JsonResponse 混用时错误响应格式容易不统一。把permission_classes []关闭掉是因为 DRF 内置的权限类按模型权限校验和我们的角色映射不兼容自定义装饰器直接在方法上执行逻辑更透明出了问题也好排查。参数说明注意csrf_exempt的位置。很多新手把它写在权限装饰器上面导致装饰器先执行、CSRF 检查后执行顺序一错接口直接 403。装饰器的执行顺序是从上往下执行函数包装csrf_exempt必须是最靠近视图函数的那个。也就是说它在最下面这些装饰器里离函数最近。3.4 权限数据的接口登录后一次性下发用户没有权限标识前端动态路由就是无米之炊。所以登录接口在验证账号密码后还要做一步聚合查询把当前用户角色集合、权限标识集合全部序列化返回给前端。这一步不要偷懒在前端做硬编码权限树的生成必须由后端算好。def generate_permission_data(user): 生成当前用户的权限数据返回给前端 roles user.user_roles.filter(role__statusTrue).select_related(role) role_codes [ur.role.code for ur in roles] perm_codes set() for ur in roles: perms ur.role.permissions.all() perm_codes.update([p.codename for p in perms]) # 超级管理员返回通配权限 if user.is_superuser: role_codes [admin] perm_codes [*] return { userId: user.id, username: user.username, roles: role_codes, permissions: list(perm_codes) }逻辑说明select_related(role)是为了防止遍历 UserRole 时对每条记录都重复查询角色表这在角色数量多的时候性能差异非常明显。权限收集用了set天然去重避免同一个权限被多个角色重复绑定时返回重复项。参数说明超级管理员的权限返回*通配符前端拿判断时只要查到*就当拥有所有权限。这个设计能大幅减少超管的权限配置工作量但要警醒的是前端放通了*后端必须用装饰器同时放通is_superuser否则界面全开了、接口照样 403那才是真正的“体验翻车”。4. 前端落地动态路由、菜单与按钮级权限控制4.1 登录态管理Token 存哪里、路由守卫怎么写前端拿到登录接口返回的 Token 和权限数据后第一件事就是决定 Token 存哪。localStorage刷新不丢、但 XSS 能偷sessionStorage标签页关闭就丢Pinia内存存储刷新就没了。我的落地习惯是Token 放localStorage权限数据和用户信息放Pinia每次页面刷新后通过一个/get_info接口重新拉取权限数据并动态注册路由。路由守卫的逻辑大概是下面这样// router/index.js import { useUserStore } from /stores/user router.beforeEach(async (to, from, next) { const token localStorage.getItem(token) if (!token) { if (to.path /login) return next() return next(/login) } if (to.path /login) return next(/) const userStore useUserStore() // 如果权限数据还没加载先拉取用户信息并动态注册路由 if (!userStore.permissions.length) { try { const res await userStore.fetchUserInfo() // 把所有动态路由加载进 router res.permissions.forEach(route { router.addRoute(route) }) return next({ ...to, replace: true }) } catch (e) { localStorage.removeItem(token) return next(/login) } } next() })逻辑说明这里有一个非常关键的设计——权限数据未加载时访问任何页面都先用await拉取信息然后addRoute动态注册路由再next({ ...to, replace: true })重新进入目标路由。如果不重新进入第一次导航已经在路由匹配阶段结束了刚注册的路由不生效页面会一直白屏或者被next(/)打到首页。参数说明userStore.fetchUserInfo()拿到的不是一个扁平列表而是一棵嵌套的路由结构父路由、子路由、组件路径、权限标识。前端需要把这棵树拆开逐个调用router.addRoute()因为 Vue Router 4 不支持一次性传入整个数组。你看到的很多网上的源码在这一步直接routes.push(...)是没用的那只是修改了本地数组Router 实例根本没感知到。4.2 菜单渲染根据权限标识过滤出可访问的菜单动态路由注册完成之后菜单栏不能和路由表直接绑定因为路由表里有一部分是嵌套的父子关系而菜单渲染需要的是层级标题、图标、路径这三个字段。我习惯单独维护一份“菜单配置树”每一条菜单都标上需要的权限标识渲染时和用户权限集合做过滤。// 菜单配置片段 const menuConfig [ { path: /system, title: 系统管理, icon: Setting, permission: system:view, children: [ { path: /system/user, title: 用户管理, icon: User, permission: user:list }, { path: /system/role, title: 角色管理, icon: Avatar, permission: role:list }, ] }, { path: /dashboard, title: 工作台, icon: HomeFilled, permission: } ] // 过滤函数递归过滤掉没有权限的菜单项 function filterMenus(menus, permissions) { return menus.filter(menu { if (menu.permission !permissions.includes(menu.permission)) { return false } if (menu.children menu.children.length) { menu.children filterMenus(menu.children, permissions) } return true }) }逻辑说明过滤逻辑用的是递归因为权限菜单通常不止两级。我遇到过一个系统做了一级菜单过滤、二级菜单直接全部展示的角色切过去以后二级菜单全部点开然后一无所获非常影响信任感。所以子菜单过滤和父菜单过滤必须同步做。参数说明权限标识为空字符串表示该菜单登录即可见不需要额外权限。这个设计在处理“首页/工作台”这类公共页面时非常有用免去了为每个角色重复配置公共菜单的麻烦。4.3 按钮级权限指令v-permission的封装菜单权限控制好了按钮级别的权限控制很多人还在用v-ifpermissions.includes(user:add)写起来累不说还容易漏。我推荐封装一个自定义指令v-permission一行代码控制按钮显隐对代码整洁度的提升非常明显。// 自定义指令 v-permission import { useUserStore } from /stores/user const permission { mounted(el, binding) { const userStore useUserStore() const requiredPerm binding.value const userPerms userStore.permissions // 超管通配符直接放行 const hasPermission userPerms.includes(*) || userPerms.includes(requiredPerm) if (!hasPermission) { el.parentNode el.parentNode.removeChild(el) } } } // 在 main.js 注册 app.directive(permission, permission)使用方式el-button typeprimary v-permissionuser:add新增用户/el-button el-button typedanger v-permissionuser:delete删除用户/el-button逻辑说明指令的mounted钩子在 DOM 挂载时执行可以拿到已经注入到 Pinia 的权限数据。removeChild直接移除这个按钮比v-if更彻底——v-if只是不渲染它的 DOM 注释节点还在移除是彻底从 DOM 树里删掉。要注意的是mounted阶段用户权限肯定已经加载完毕因为前面路由守卫已经把数据拉取阻塞住了。参数说明v-permission的值为字符串时对应单个权限标识。有的按钮需要同时拥有多个权限才算有权限那就要给指令扩展数组参数的支持代码里加一个Array.isArray分支即可日常项目里单权限场景占大多数先把基础的写好再扩展。4.4 前端权限与后端接口的联动要点权限数据流最容易被忽略的环节是“前端 perm 标识变了的同步问题”。角色改了权限后端数据库更新了但前端已经登录的用户拿到的还是旧 permissions 列表理论上这是正常的——权限配置变更通常要重新登录才生效或者后端在每次请求时强制刷新。我在实际项目中采用过一个简单方案每次前端路由切换时后台静默调用/get_info刷新一次权限列表如果发现当前页面已无权限自动跳转 403。体验上权限变更在几十秒内就能生效又不会频繁打扰用户。重要提示前端addRoute的动态路由在用户刷新页面后是需要重新执行的如果此时/get_info接口报错或者 Token 过期路由守卫要把用户踢回登录页而不是放任一个空白页面。5. 权限系统避坑四个高频翻车现场5.1 逻辑删除与角色权限唯一约束冲突现象给角色表加了一个is_active字段做软删除然后重新创建一个同名的角色结果报错uniq约束冲突旧角色根本“删不掉”。原因数据库层的唯一约束建立在整个表上逻辑删除并没有真正移除记录新数据插入时唯一索引依然认为旧记录存在。解决唯一约束改为针对活跃记录生效。最稳妥的办法是删掉数据库里的uniqueTrue约束改成在代码层每次新增角色前先查一下是否存在同名活跃角色如果项目用的是 PostgreSQL还可以用部分唯一索引解决。MySQL 下最实用的是代码层校验加一个exists()查询牺牲极少性能换取灵活性。5.2 Token 过期后前端路由残留和菜单残留现象用户长时间挂机Token 过期后重新登录了一个低权限账号结果发现菜单里还残留着上一个高权限账号的功能项。原因Pinia 里的权限列表在换账号时没有清空动态路由用addRoute注册进去后不会自动注销。解决登录接口返回成功时的第一时间先调用logout清理逻辑——把localStorage的 Token、Pinia 的 permissions 全部重置成初始值再拉取新账号的数据。Vue Router 4 没有提供“移除全部动态路由”的 API所以更彻底的做法是登录成功后调用router.replace(/)并强制刷新页面让整个应用重新挂载动态路由从零重建。这个刷新动作看起来粗暴但确实是规避路由残留最简单可靠的手段。5.3 权限查询引发的 N1 性能问题现象用户列表接口越跑越慢打开数据库慢查询日志发现每查一个用户都要额外查他对应的角色和权限用户一多就是几十条 SQL。原因生成权限数据时用了循环内查询for ur in roles里再查role.permissions三层嵌套 N1 问题叠在一起。解决用prefetch_related一次性把角色关联的权限查出来预加载。改一行代码的事情user.user_roles.filter(...).select_related(role).prefetch_related(role__permissions)。这条优化在用户量过千以后效果立竿见影SQL 数量从“用户数乘以角色数”降为固定的几条。5.4 自定义装饰器被超级管理员绕过时漏掉状态判断现象所有超管接口都能访问但后来发现一个被停用的超管账号依然能访问所有接口。原因装饰器里写了if user.is_superuser: return view_func(...)但没判断user.is_active和user.user_roles关联的角色是否被停用。解决超管判断放在用户状态判断之后。先确认user.is_active为真、再查token是否有效、最后才判断超管身份。停用的用户本质上连认证都不该通过这和角色被停用是两码事。我在给某后台系统做权限加固时把这个顺序调整过风险立刻消除。6. 从工程到论文把源码沉淀成可答辩的落地成果6.1 源码目录怎么组织才不像“杂货铺”一套权限管理系统的源码目录组织直接决定了代码评审的第一印象。我建议前端和后端分两个根目录后端按 Django 的标准 app 拆分单独拎出一个rbacapp 负责用户、角色、权限相关的模型与接口前端按views、router、stores、directives分层。核心的权限校验装饰器、路由守卫、权限指令都集中放置代码里注释写清楚“这段代码是权限核心逻辑不要随意改动”。6.2 论文的核心章节和代码怎么对应拿到源码写论文时不要按功能模块平铺而要按“问题—模型—实现—验证”的思路组织。第一章写权限管理的背景和现状第二章写 RBAC 模型在本系统中的设计数据表结构、角色关系图第三章写后端实现Token 认证、自定义装饰器、接口聚合第四章写前端实现动态路由、菜单渲染、按钮指令第五章写系统测试。每一章都要能对应到源码里的具体文件“从论文能反查到代码位置”是答辩时最有说服力的状态。6.3 答辩演示的验收路径演示环节建议按这条路径走一遍用超管账号登录创建两个角色——一个只分配“查看”权限一个分配“增删改”权限创建两个对应用户分别登录演示菜单差异、按钮差异、直接访问无权限 URL 时后端返回 403最后以超管身份修改角色权限重新登录后确认权限变化生效。走完这一条流程评审能直观看到 RBAC 的核心价值——权限是动态的、数据驱动的而不是写死在代码里的。我自己做权限模块踩得最深的坑就是一开始把前端按钮隐藏当成“权限控制完成”直到被反问了一句“如果绕过前端直接调接口呢”才意识到安全边界在哪。从那以后我给自己定了条规矩前端权限只谈体验后端鉴权才是底线。希望这篇拆解能帮你在做这套 Django Vue.js 的 RBAC 系统时少走这些弯路把源码和论文都做成自己真正讲得清、改得动的成果。本文还有配套的精品资源点击获取