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

Django+Vue3构建RBAC权限管理系统:从JWT认证到动态路由

简介一份基于 Python Django 后端与 Vue 3 Element-Plus 前端构建的前后端分离单体权限管理系统资源适合具备一定 Web 基础、希望掌握前后端分离开发与权限控制实战的开发者。压缩包共 70 个文件以 65 个 Python 文件为核心涵盖 Django 项目配置、业务应用、中间件与工具脚本另有 README、requirements.txt、.gitignore 与 LICENSE 等辅助文件整体仅 121KB便于快速预览和学习。已有 137 人学习下载。从文件结构看项目包含 youlai_django 主配置system 应用下有 users、roles、depts、menus、logs 等模块并集成认证授权、字典、文件与通知功能体现了清晰的分层和模块化设计。通过阅读源码可掌握 Django RESTful API 开发、Vue 3 组合式 API 与 Element-Plus 组件库的整合方式也可作为实际单体权限管理系统的搭建参考和二次开发基础。 做后台管理系统权限控制始终是绕不开的核心模块。现在前后端分离的项目需求特别多技术选型翻来覆去对比Python Django Vue 3 Element-Plus 这套组合在中小型项目里出镜率相当高既能快速交付又有清晰的工程边界。这篇文章把我从零搭建一套完整权限管理系统的过程整理出来涵盖RBAC模型设计、JWT认证、动态路由、按钮级权限控制、前后端联调和部署的完整链路以上都是实测跑通的思路和代码可以直接参考复现。适合刚接触前后端分离开发的同学也适合想自己搭一套后台权限模板的开发者。1. 项目概述与整体设计思路1.1 技术选型为什么是 Djanggo 配 Vue 3Django 在后台权限管理这个领域有天然优势自带的 Admin 后台、ORM、用户认证体系非常成熟写业务接口的效率很高。Python 这边的生态又特别全做内部系统、中后台、运维平台没有比 Django 更顺手的框架之一。Vue 3 加上 Element-Plus 则负责前端界面组件库覆盖表格、表单、弹窗、树形控件这些后台管理的高频场景开箱即用UI 风格也统一。有个细节值得说Vue 3 现在默认用 Composition API配合script setup写起来比 Vue 2 时代简洁很多。Element-Plus 对 Vue 3 的适配目前已经非常稳定按需引入做完了首屏体积也控制得住。我这套项目前端部署完静态资源 gzip 之后大概只有 300 多 KB加载速度很理想。1.2 单体架构的选择与边界划分有人可能会问既然做权限管理系统是不是直接用微服务更先进我的答案是看场景。权限管理系统本质上是企业内部后台用户量、数据量都有限单体架构部署简单、排查问题快、开发效率高一个容器或者一台服务器就能跑起来。微服务带来的服务治理、分布式事务成本在这种场景下完全是负担。所以这里选了前后端分离的单体应用架构后端是 Django 单体服务提供 RESTful API前端是独立的 Vue 3 工程通过 HTTP 请求和后端交互。部署的时候 Nginx 托管前端静态文件并反向代理 API 请求整个系统只占一个 Web 服务端口。这个架构既享受了前后端分离的开发体验又避免了微服务的运维复杂度是性价比最高的方案。1.3 RBAC 权限模型设计三级权限体系权限模型我采用经典的 RBAC基于角色的访问控制设计核心思路是用户不直接关联权限而是通过角色间接获得权限。这样做的好处是当有新员工入职时只需要给他分配一个运营角色所有菜单和操作权限自动生效不用一条条配置。具体到实现我把它拆成三层菜单权限控制用户能看到哪些左侧菜单和页面路由。按钮权限控制页面内新增、编辑、删除、导出这类操作按钮是否可见或可用。接口权限后端接口校验请求者是否拥有对应的权限码这是最后一道防线。这三层分别由前端路由守卫、自定义指令和后端权限类去控制缺一不可。数据库层面我设计了五张核心表用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。菜单表里用 parent 字段自关联形成树形结构每个菜单节点上挂一个 perm_code 权限标识比如user:add、user:delete后端接口校验用的就是这些标识。2. 环境准备与工程初始化2.1 Python 与 Django 工程初始化环境准备这块建议直接用虚拟环境隔离项目依赖。我用的是 Python 3.10Django 4.2 LTS这个版本组合经过长期验证稳定性很好。python3 -m venv venv source venv/bin/activate pip install django djangorestframework djangorestframework-simplejwt django-cors-headers django-admin startproject permission_system cd permission_system python manage.py startapp users依赖包里简单解释一下djangorestframework是 DRF负责写 APIdjangorestframework-simplejwt提供 JWT 认证django-cors-headers解决开发环境的跨域问题。这些是目前 Django 做前后端分离项目的标准配置。工程建好后记得去settings.py里注册 app把rest_framework、corsheaders、users都加进INSTALLED_APPS。然后执行数据库迁移初始化python manage.py migrate python manage.py createsuperuser2.2 Vue 3 Element-Plus 工程搭建前端工程我推荐用 Vite 创建速度比 Webpack 快非常多开发体验完全不一样。创建命令如下npm create vitelatest frontend -- --template vue cd frontend npm install npm install element-plus element-plus/icons-vue axios vue-router pinia项目结构按模块划分我习惯这样组织frontend/src ├── api/ # 接口请求分类封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── directives/ # 自定义指令 ├── router/ # 路由配置 ├── stores/ # Pinia 状态管理 ├── utils/ # 工具函数 ├── views/ # 页面组件 ├── App.vue └── main.jsElement-Plus 的引入方式我建议按需引入配合unplugin-auto-import和unplugin-vue-components两个插件组件和 API 都能自动导入代码量明显减少。全量引入虽然省事但打包体积大首屏加载慢不推荐。2.3 前后端目录结构与端口规划工程目录整体规划如下permission_system/ ├── backend/ # Django 后端 │ ├── permission_system/ # 项目配置 │ ├── users/ # 用户模块 │ ├── rbac/ # 权限模块 │ ├── static/ # 静态文件 │ └── manage.py ├── frontend/ # Vue 3 前端 │ ├── src/ │ ├── vite.config.js │ └── package.json └── deploy/ # 部署配置端口规划方面开发环境后端跑8000前端 Vite 跑5173用 Vite 的 proxy 把/api开头的请求转发到后端。生产环境统一由 Nginx 监听80或443前端静态文件放在 Nginx 站点目录API 请求通过反向代理转发到本地8000端口。这样整个系统对外就一个入口配置简单也安全。3. 后端核心实现用户、角色与权限模型3.1 自定义用户模型与数据表设计Django 默认的 User 模型字段有限缺少手机号、部门、头像这些企业后台常用字段所以第一件事就是自定义用户模型。先在users/models.py里继承AbstractUser然后必须在迁移之前把AUTH_USER_MODEL配置好否则后面该起来非常麻烦from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): phone models.CharField(max_length11, blankTrue, verbose_name手机号) avatar models.URLField(blankTrue, verbose_name头像) dept models.CharField(max_length50, blankTrue, verbose_name部门) is_active models.BooleanField(defaultTrue, verbose_name是否启用) class Meta: db_table sys_user verbose_name 用户在settings.py里配置AUTH_USER_MODEL users.User角色模型和菜单模型我放在rbac这个 app 里用 ManyToManyField 建立关联class Menu(models.Model): parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE, verbose_name父级菜单) title models.CharField(max_length50, verbose_name菜单名称) path models.CharField(max_length100, blankTrue, verbose_name路由地址) icon models.CharField(max_length50, blankTrue, verbose_name图标) sort models.IntegerField(default0, verbose_name排序) perm_code models.CharField(max_length100, blankTrue, verbose_name权限标识) type models.CharField(max_length10, choices((dir,目录),(menu,菜单),(button,按钮)), defaultmenu, verbose_name类型) class Role(models.Model): name models.CharField(max_length30, uniqueTrue, verbose_name角色名称) remark models.CharField(max_length200, blankTrue, verbose_name备注) menus models.ManyToManyField(Menu, related_nameroles, verbose_name关联菜单)用户和角色的关联我建议通过自定义中间表来做虽然默认的 ManyToManyField 也能用但自定义后可以在关联表里加字段比如分配时间、操作人后续扩展空间大。这里我在 Role 上面通过users字段已经建立了 M2M 关联数据库会自动生成中间表为了简单也可以先这样用。3.2 JWT 认证接入与自定义权限类JWT 认证配置在settings.py里的 DRF 配置块中REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], }登录接口我直接复用simplejwt的TokenObtainPairSerializer然后重写一下返回内容把用户信息、菜单列表一起返回给前端省得前端再单独请求一次用户信息接口。登录成功后拿到access_token和refresh_token前端把 access_token 存 localStorage每次请求头带上Authorization: Bearer token。后端的接口权限校验用 DRF 自定义权限类实现。每个视图里定义perms属性权限类负责核对当前用户是否拥有权限标识from rest_framework import permissions class PermCodePermission(permissions.BasePermission): def has_permission(self, request, view): perms getattr(view, perms, []) if not perms: return True user_perm_codes set() for role in request.user.roles.all(): codes role.menus.exclude(perm_code).values_list(perm_code, flatTrue) user_perm_codes.update(codes) return any(perm in user_perm_codes for perm in perms)在视图里这样使用class UserDeleteView(APIView): permission_classes [IsAuthenticated, PermCodePermission] perms [user:delete] def delete(self, request, pk): User.objects.filter(pkpk).delete() return Response({code: 0, msg: 删除成功})注意request.user是 JWT 认证成功后自动注入的当前登录用户DRF 会在请求进来时先做认证再做权限判断顺序不要搞反。3.3 菜单与接口权限的打通菜单接口的设计对前端动态路由至关重要。登录成功后前端需要拿到当前用户可见的菜单树后端接口返回的是符合前端路由结构的数据。我的做法是先查角色菜单再组装成前端需要的树形 JSONdef build_menu_tree(menus): tree [] menu_map {m[id]: m for m in menus} for m in menus: m[children] [] if m[parent_id] is None: tree.append(m) else: parent menu_map.get(m[parent_id]) if parent: parent[children].append(m) return tree这个菜单树包含每个菜单的path、component前端组件路径、title、icon。前端拿到之后用router.addRoute()动态注册路由同时渲染左侧菜单栏。权限标识不放在菜单树里单独传而是在前端登录后通过另一个接口一次性获取用于按钮级权限判断这样菜单数据和权限数据职责分离更清晰。4. 前端核心实现登录、动态路由与按钮权限4.1 Axios 请求封装与 Token 处理前端所有请求都走统一封装的 axios 实例这一步不做的话后面每个页面写 token 处理会非常痛苦。我的封装逻辑如下import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 10000, }) // 请求拦截器自动携带 token service.interceptors.request.use(config { const token localStorage.getItem(access_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理错误码 service.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.clear() router.push(/login) ElMessage.error(登录已过期请重新登录) return Promise.reject(new Error(Unauthorized)) } if (res.code ! 0) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )这里有个经验系统里的所有接口统一返回{ code: 0, msg: success, data: ... }的结构code 为 0 代表成功非 0 代表业务错误HTTP 状态码只保留 401、403 这类少数场景使用。这样前端处理逻辑非常简单不用每个接口单独写错误判断。4.2 登录流程与动态路由的实现登录流程是权限系统前端最核心的部分我的实现逻辑是用户输入用户名密码调用/auth/login接口。后端返回 access_token、refresh_token 和用户信息。前端存好 token调用/user/menus获取菜单树。遍历菜单树用router.addRoute()动态注册路由。路由注册完成后当前页面通过router.replace()跳转到首页。动态路由的核心代码function registerDynamicRoutes(menus) { const viewsModules import.meta.glob(/views/**/*.vue) const flatMenus flatten(menus) flatMenus.forEach(menu { if (!menu.component) return const routerRecord { path: menu.path, name: menu.path.replace(/\//g, -), component: viewsModules[/src/views/${menu.component}.vue], meta: { title: menu.title, icon: menu.icon }, } router.addRoute(routerRecord) }) }需要注意import.meta.glob的引入方式这是 Vite 的静态依赖分析机制不能写成动态拼接变量去import()否则打包时模块会被遗漏。如果你有嵌套路由需求建议构建路由时先处理父子关系父级用 Layout 布局组件包一层子路由挂到 children 上这样页面能嵌套在侧边栏布局内。4.3 按钮级权限v-permission 指令实战按钮权限我实现了一个自定义指令比在每个页面手动v-if权限判断要优雅得多import { usePermissionStore } from /stores/permission export const hasPerm (perms) { const permissionStore usePermissionStore() if (!permissionStore.permCodes.length) return false return Array.isArray(perms) ? perms.some(p permissionStore.permCodes.includes(p)) : permissionStore.permCodes.includes(perms) } export const permission { mounted(el, binding) { if (!hasPerm(binding.value)) { el.parentNode?.removeChild(el) } }, }在main.js里注册import { permission } from /directives/permission app.directive(permission, permission)页面上直接这样用el-button v-permissionuser:add typeprimary新增用户/el-button el-button v-permission[user:edit, user:reset]编辑/el-button菜单权限管理页面里配置角色权限时左侧是菜单树勾选后保存的是菜单 id 列表。提交到后端时后端会把勾选的按钮类型节点的perm_code一并落库下次用户登录获取权限码时自然包含这些按钮权限。4.4 Element-Plus 高频组件使用要点Element-Plus 在权限管理页面里用得最多的是表格、表单、弹窗、树形控件和分页。我总结几个要点表格加分页是老套路了加载数据时用loading属性分页事件重新调用请求接口不要把分页参数忘在请求里。表单校验用rules配置注意prop必须和el-form-item的prop一一对应否则校验不生效。编辑弹窗我习惯用el-dialog套el-form每次打开时用nextTick重置表单const handleEdit (row) { form.value { ...row } dialogVisible.value true nextTick(() { formRef.value?.clearValidate() }) }树形控件在权限分配页面用处很大配置show-checkbox和node-key选中后通过getCheckedKeys()和getHalfCheckedKeys()合并提交这样才能保证父节点勾选状态正确保存。Element-Plus 的树形控件还支持懒加载菜单层级深的时候能减少首屏数据量。5. 联调部署与常见问题实录5.1 跨域处理开发环境与生产环境两种方案前后端分离开发中跨域不可回避。我采用开发环境代理、生产环境反向代理的方案不在后端开 CORS 跨域。开发环境在vite.config.js里配置export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true, }, }, }, })这样前端把请求发到/api/xxxVite 会自动转发到后端8000端口浏览器里不存在跨域问题。生产环境由 Nginx 做同样的转发后面部署配置里会写。如果你非要用 CORS 方案后端开django-cors-headers也简单但生产环境下如果 API 接口被其他域名跨域调用会带来安全风险所以我也只在纯内网开发工具里才会开。5.2 前端打包与部署配置npm run build构建前端之后把 dist 目录托管到 Nginx 站点目录后端 Django 用python manage.py collectstatic收集静态文件。部署 Nginx 配置的关键点如下server { listen 80; server_name your-server-domain.com; client_max_body_size 20m; root /var/www/frontend/dist; index index.html; # 前端路由 history 模式必须加否则刷新页面 404 location / { try_files $uri $uri/ /index.html; } # API 反向代理 location /api/ { 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; } location /static/ { alias /var/www/backend/static/; } }Django 后端用gunicorn启动绑定127.0.0.1:8000不直接对外暴露。注意settings.py里DEBUG FalseALLOWED_HOSTS配置服务器域名STATIC_ROOT和STATIC_URL要配置正确否则静态文件会 404。5.3 常见问题速查与排查思路我把这个项目开发过程中踩过的坑按优先级整理成速查表问题现象根本原因解决思路前端刷新页面 404路由 history 模式下 Nginx 没有回退到 index.html配置try_files $uri $uri/ /index.html登录成功但动态路由不生效登录后没有调用addRoute或者路由注册顺序不对确认登录流程中先拿到菜单数据再 addRoute最后next()跳转接口返回 403权限类校验失败或者用户角色没关联菜单权限码检查perms是否在视图正确配置用get_user_perms调试打印权限码列表修改角色后权限不生效用户 token 里缓存了旧权限或者权限码直接从数据库取时缓存没刷新JWT 无状态需重新登录如果用的是 DRF 自带权限校验则每次实时查库无需担心Element-Plus 组件样式异常按需引入配置不完整检查unplugin-vue-components的 resolver 是否正确配置时间字段序列化到前端差 8 小时时区设置问题USE_TZ True但数据库存的是 UTC 时间统一使用Asia/Shanghai时区USE_TZ False或序列化时用%z格式排查动态路由白屏问题时我建议打开浏览器控制台看路由注册情况router.getRoutes()输出一下就能确认是否注册成功。权限码获取失败时先看/user/perms接口返回的数据是否完整再检查角色是否勾选了按钮权限并保存成功。还有一个小坑想重点提一下前后端联调时access_token过期时间我设置的是 30 分钟前端没有做自动刷新 token 的逻辑。现在简单做的话在响应拦截器里判断HTTP 401时调用/auth/refresh接口用refresh_token换取新 token然后重放失败请求。如果项目再复杂一些可以用 axios 的请求队列实现并发刷新不过对于单体后台系统简单刷新就够用了。最后再分享一个我在前后端分离权限开发里的体会先把主流程跑通再丰富细节。先从用户登录、菜单获取、动态路由这几步走过来页面先不做按钮级别的权限后面再一步步加 v-permission 指令和接口权限校验。这个系统做下来最花时间的不是写代码而是权限数据结构和角色分配策略的思考这部分想清楚了实现起来其实就是流水线作业。如果哪天你想扩展成多租户模式在角色上挂一个租户外键查询时多一个过滤条件就能实现这套 RBAC 基础的扩展性并不会成为瓶颈。本文还有配套的精品资源点击获取
分享:

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

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