简介:面向计算机、软件工程专业的毕业设计需求,这是一套基于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.js:RBAC 的实现分工与选型边界
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_length=20, blank=True) class Meta: db_table = 'sys_user' class Role(models.Model): name = models.CharField(max_length=50, verbose_name='角色名称') code = models.CharField(max_length=50, unique=True, verbose_name='角色标识') description = models.CharField(max_length=255, blank=True) status = models.BooleanField(default=True, verbose_name='是否启用') # 角色与权限的多对多关系 permissions = models.ManyToManyField( Permission, blank=True, related_name='roles_in_perm', verbose_name='权限列表' ) class Meta: db_table = 'sys_role' # 用户与角色是多对多,放在用户模型上直接声明 # 但为了后期扩展,单独建一个中间模型更灵活 class UserRole(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_roles') role = models.ForeignKey(Role, on_delete=models.CASCADE, related_name='role_users') created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'sys_user_role' unique_together = ('user', 'role')逻辑说明:用户通过 UserRole 中间表和角色关联,角色通过自带的 ManyToMany 和权限关联,这就在数据库层面形成了“用户—角色—权限”的经典 RBAC 三角关系。related_name必须写清楚,不然默认的反向查询名称会和 Django 自带的用户权限字段冲突。
参数说明:角色标识code加上unique=True是为了在前端用字符串判断权限时不至于拿着中文名到处比较,例如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(key=token_key).user except Token.DoesNotExist: return JsonResponse({'code': 401, 'msg': '登录已过期'}, status=401) if not user: return JsonResponse({'code': 401, 'msg': '未登录'}, status=401) # 超级管理员直接放行 if user.is_superuser: return view_func(request, *args, **kwargs) # 查询用户所有角色的权限标识 has_perm = user.user_roles.filter( role__status=True, role__permissions__codename=perm_code ).exists() if not has_perm: return JsonResponse({'code': 403, 'msg': '无权限访问'}, status=403) 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 原生视图函数和 DRF(Django 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': '请求方式不支持'}, status=405) # 方式二: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_active=True) # 序列化并返回用户列表 data = [{'id': u.id, 'username': u.username} for u in users] return Response({'code': 200, 'data': data}, status=status.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__status=True).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-if="permissions.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 type="primary" v-permission="'user:add'">新增用户</el-button> <el-button type="danger" v-permission="'user: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约束冲突,旧角色根本“删不掉”。
原因:数据库层的唯一约束建立在整个表上,逻辑删除并没有真正移除记录,新数据插入时唯一索引依然认为旧记录存在。
解决:唯一约束改为针对活跃记录生效。最稳妥的办法是删掉数据库里的unique=True约束,改成在代码层每次新增角色前先查一下是否存在同名活跃角色;如果项目用的是 PostgreSQL,还可以用部分唯一索引解决。MySQL 下最实用的是代码层校验,加一个exists()查询,牺牲极少性能换取灵活性。
5.2 Token 过期后,前端路由残留和菜单残留
现象:用户长时间挂机,Token 过期后重新登录了一个低权限账号,结果发现菜单里还残留着上一个高权限账号的功能项。
原因:Pinia 里的权限列表在换账号时没有清空,动态路由用addRoute注册进去后不会自动注销。
解决:登录接口返回成功时的第一时间,先调用logout清理逻辑——把localStorage的 Token、Pinia 的 permissions 全部重置成初始值,再拉取新账号的数据。Vue Router 4 没有提供“移除全部动态路由”的 API,所以更彻底的做法是登录成功后调用router.replace('/')并强制刷新页面,让整个应用重新挂载,动态路由从零重建。这个刷新动作看起来粗暴,但确实是规避路由残留最简单可靠的手段。
5.3 权限查询引发的 N+1 性能问题
现象:用户列表接口越跑越慢,打开数据库慢查询日志发现每查一个用户都要额外查他对应的角色和权限,用户一多就是几十条 SQL。
原因:生成权限数据时用了循环内查询,for ur in roles里再查role.permissions,三层嵌套 N+1 问题叠在一起。
解决:用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 系统时少走这些弯路,把源码和论文都做成自己真正讲得清、改得动的成果。
本文还有配套的精品资源,点击获取