☰
Element Admin后台权限管理系统与CMS内容管理实践解析
2026/10/20 15:26:50 网站建设 项目流程

接手过那么多管理后台项目,我一直有个很深的感触:权限管理和内容管理看着是两件事,但在真实业务里它们总是绑在一起出现。后台这边要控制谁能进哪个菜单、谁能点哪个按钮,前台那边要有人发文章、传图片、管理分类,两边还共用一套账号体系。所以当团队决定用Element Admin这套方案同时解决权限和CMS两块需求时,我是很支持的,因为这正好戳中了大多数中后台项目的核心痛点。

这篇文章就围绕ElementAdmin后台权限管理系统+CMS管理系统这个组合来写,面向两类人:一类是准备从零搭建后台管理系统的Vue开发者,另一类是已经跑通Demo但卡在权限设计、CMS内容模型这些细节上的同学。我把整个过程中的关键设计思路、实操步骤、踩坑记录都整理出来,尽量让文章能直接照着落地。

1. 为什么几乎每个Vue团队最终都会绕回Element Admin

先聊一个可能被很多人忽略的事实:市面上开源后台管理模板多到数不清,但Element Admin这个系列的生命力一直很顽强。不是因为它代码写得有多惊艳,而是它刚好站在一个非常稳定的交叉点上——Vue的响应式机制、Element UI的组件生态、以及中后台页面的固有交互模式。

1.1 从零手写后台和基于模板改造,差了不止一个数量级

我自己刚入行那会儿也干过从零写后台的事,说实话,挺累的。登录页、布局框架、侧边栏折叠、面包屑导航、表格分页、弹窗表单、树形选择器,这些组件逻辑看起来不难,但串起来之后每个细节都在消耗时间。尤其是菜单权限和按钮权限要同时生效,需要路由守卫、动态路由表、指令系统多层配合,稍不留意就顾此失彼。

Element Admin这类模板的价值在于,它把这层和业务无关的底层基础设施提前打磨过了。你要做的不是重新发明轮子,而是理解它的轮子是怎么转的,然后把自己的业务组件装上去。对于大多数中小团队来说,这是性价比最高的路,没有之一。

1.2 这个组合到底解决了什么问题

把ElementAdmin后台权限管理系统和CMS管理系统放在一起看,本质上是做了一套完整的“内容运营中台”。它同时覆盖了三层诉求:

  • 访问控制层:解决“谁能进入系统、谁能看到哪些页面、谁能操作哪些按钮”的权限模型问题;
  • 内容生产层:解决“文章、分类、标签、封面图、发布状态”等内容数据的结构化建模问题;
  • 协作流程层:解决“编辑提交、审核通过、定时发布、下线归档”这类多人协作的流程状态问题。

这三层诉求对应到技术上,就是动态路由、状态机、指令权限、RBAC模型这些核心概念。下面我把每一块拆开讲,尽量讲细一点,因为真正的坑往往不在概念本身,而在概念落地的过程中。

2. 权限系统不是“登录后放行”,而是从按钮级权限到动态路由的全链路设计

不少同学理解的权限系统就是“登录之后根据角色生成不同的菜单”,这个理解不够完整。菜单只是权限的可见性维度,真正难的是操作维度和数据维度。在Element Admin实际的开发中,我通常把权限链路拆成四个层次,一层层去实现。

2.1 第一层:登录认证与用户会话的建立

登录流程在Element Admin里一般做成这样:前端拿到用户名和密码,调后台的登录接口,后台验证通过后返回一个token,前端把这个token存到cookie或者localStorage里,后续所有请求都通过请求拦截器自动带上。

这里有几个细节容易出现脏数据的问题:

  • token过期时间:个人建议token有效期设短一点,比如2小时,配合refresh_token做无感续期。只用一个长期有效的token,虽然开发时省事,但一旦泄漏,问题会很大。
  • 用户信息缓存:登录成功后不要把用户的所有信息都塞到vuex里,尤其是角色和权限列表这种会变的数据。正确做法是只存一个用户唯一标识,菜单和权限通过接口实时获取。
// vuex中推荐保存的结构 const state = { token: getToken(), userId: null, name: '', avatar: '', roles: [], // 每次刷新重新拉取,不从storage恢复 permissions: [] // 按钮级权限码列表,同样实时获取 }

很多项目在刷新页面之后菜单闪一下或者直接白屏,就是因为用户信息被持久化到了localStorage,刷新时从本地恢复导致整个权限链路对不上。

2.2 第二层:角色-菜单-权限码的表驱动设计

后端的角色模型通常是经典的RBAC(基于角色的访问控制):用户关联角色,角色关联菜单和权限资源。落到前端,我们需要做的就是拿到当前的菜单列表和权限码列表,然后动态生成路由。

菜单表的设计有一些通用的字段,建议在数据库层面就规划好:

字段说明示例
menu_id菜单唯一标识101
parent_id父级菜单ID0表示顶级
path前端路由路径/content/article
component前端组件路径content/article/index
title菜单名称文章管理
icon菜单图标el-icon-document
sort_order排序值1
perms_code权限码content:article:list
visible是否显示在侧边栏1

这个表不是只给后端用的,前端拿到返回结果后,只需要做一个简单的递归构建,就能生成路由和菜单树。

2.3 第三层:动态路由的加载时机和刷新保活

动态路由的核心原则是:路由表不是写死在代码里的,而是用户登录之后根据权限数据现拼出来的。

在Vue Router 4.x版本下,比较稳的做法是使用addRoute逐个添加。这里面的一个关键操作是,在路由守卫中判断当前路由表是否已经初始化,如果没有就先拉取权限数据、生成路由表、再放行;如果已经初始化则直接放行。

刷新页面白屏是动态路由最常见的坑。原因很简单:刷新后vuex重置了,动态路由丢失,用户访问的当前路径暂时匹配不上任何路由。解决办法通常是两级路由保底——在守卫里用to.matched.length === 0作为兜底条件,先把路由重新挂载,然后用next({ ...to, replace: true })重新进入一次。

2.4 第四层:按钮级权限不能只靠隐藏,后端校验才是底线

按钮级权限一般用自定义指令实现。Element Admin生态里常见的做法是注册一个v-permission指令,传入需要的权限码数组,如果没有权限就直接把DOM节点移除,防止用户看到按钮。

const permission = { mounted(el, binding) { const { value } = binding const storeUserPermissions = store.getters.permissions const hasPermission = storeUserPermissions.some(p => value.includes(p)) if (!hasPermission) { el.parentNode && el.parentNode.removeChild(el) } } }

但我在实际项目里一直强调一个原则:前端按钮隐藏只是用户体验问题,真正的权限控制必须在后端校验。因为前端的任何控制都可以被绕过,恶意用户直接调接口就能操作。所以按钮维度控制的是交互入口,接口维度的越权校验才是安全底线。

3. CMS模块与权限体系结合的底层设计思路

CMS管理系统接进来之后,系统就不再只是一个“管理后台壳子”了,它开始承担真正的内容生产业务。这一块我建议按“内容模型-分类树-状态机-审核流”四步走,每一步都和权限体系发生关系。

3.1 内容模型用单表还是分表,取决于内容类型是否固定

如果系统里只有“文章”一种内容,那设计一张article表就够了。如果打算后面支持“文章、图集、视频、下载资源”多种内容,就不要硬塞进一张表,而是设计一张统一的内容主表,加上扩展属性表。

以文章表为例,核心字段包括:

  • id主键
  • title标题
  • category_id所属分类
  • author_id作者
  • cover封面图URL
  • content正文内容
  • status状态码
  • audit_user审核人
  • audit_time审核时间
  • publish_time发布时间
  • create_time/update_time创建和更新时间

一个比较容易被忽略的字段是status,它既是内容状态,又是审核流程的判断依据。我习惯用整数状态码来标记,而不是直接存字符串,因为整数在查询索引和前后端交互上都更高效。

3.2 分类树的两种实现方式

CMS里几乎都离不开分类。分类通常有两种形态:一种是固定层级的树形结构,一种是无层级的标签式结构。对于文章类CMS,我建议用树形结构,因为分类天然有父子关系,比如“公司动态”下面可能有“产品发布”、“团队活动”这种子分类。

树形结构在数据库里最简单的存法是parent_id自关联,查询时一次性取出全部分类,在Java或者JavaScript层组装成树。不推荐在SQL里反复递归查询,因为分类数据量不会特别大,一次性取出后在内存里组装反而更快。

前端用Element UI的el-tree组件渲染分类树,支持选中和勾选两种模式。这里有个小经验:如果只是单选文章所属分类,用node-click事件;如果需要批量操作(比如按分类批量审核),才需要开启show-checkbox支持多选。

3.3 文章状态机:草稿、待审核、已发布、已下线

CMS的状态设计没有统一标准,但至少要覆盖内容从创建到生命周期结束的几个基本阶段。我通常会用下面这个状态流转:

草稿(0) -> 待审核(1) -> 已发布(2) -> 已下线(3) \ / -> 驳回(4)

每个状态的触发动作不同:编辑可以创建草稿、提交审核;管理员或审核人可以把待审核的文章变为已发布或驳回;发布后的文章可以手动下线,也可以设置定时自动下线。

状态字段加上审核人、审核时间之后,整个内容的审计链路就完整了。这块对于做运营的后台非常关键,不然出了问题根本不知道是哪一步、哪个人弄的。

3.4 权限与控制流怎么绑在一起

CMS模块的权限不再只是“能不能看见菜单”,而是要细化到“能不能审核”、“能不能发布”。我的做法是利用权限码来做流程控制:

  • 编辑角色拥有权限码:cms:article:add、cms:article:edit、cms:article:submit
  • 审核角色拥有权限码:cms:article:audit、cms:article:reject
  • 管理员拥有全部权限,外加cms:article:delete、cms:article:offline

前端在操作按钮上绑好这些权限码,后端在接口上做好角色校验,双管齐下。这样一个编辑账号就算绕过前端拿到了审核接口地址,后端也会拒绝,因为他没有对应的角色和权限码。

4. 从Demo跑通到业务上线,绕不开的五个实际问题

模板项目跑起来很容易,npm install加npm run dev,两分钟就能看到一个完整的后台界面。但从Demo到真正上线,中间踩过的坑能写满一屏。我只挑自己项目中真实遇到、并且花了不少时间解决的问题来说,每一个都是典型的“不看文档就会掉进去”的坑。

4.1 菜单数据是从后端返回还是前端写死

很多模板项目会把菜单直接写在路由配置里,然后用角色去过滤。这种方式在系统菜单固定时没问题,但如果菜单要支持动态配置(运营可以在后台新增一个导航入口),那就必须用后端动态返回的菜单数据。

我的最终方案是:默认菜单写在前端,作为静态路由;扩展菜单存后端,作为动态路由。这两种路由在守卫里合并,先加载静态,再加上动态。这样系统的核心页面不会被后台配置搞挂,扩展页面又保持了灵活性。

4.2 axios请求封装不是简单封装get和post

Element Admin模板一般自带axios封装,但很多封装只是做了基本的拦截。实际项目中,至少需要在这些方面做增强:

  • 统一错误处理:HTTP 401跳登录页;403提示无权限;500弹后端返回的错误消息。
  • 业务错误码处理:后端接口经常返回{ code: 200, data: ... }这种结构,code不等于200时也要进入错误分支。
  • 重复请求屏蔽:针对提交类的接口,在Pending期间禁止再次点击,防止表单重复提交。
  • 取消失效请求:在路由切换时取消之前未完成的请求,节省连接资源。

重复提交这个问题,很多项目到上线之后才暴露出来。用户双击提交按钮,结果创建了两条一模一样的文章记录。解决方式也很简单,封装一个requestLock,同一个接口同一个入参在短时间内只允许发出一次。

4.3 按钮权限码的维护和组织方式

权限码最容易乱在命名上。有人用中文备注,有人用随机字符串,有人直接不建表,写死在代码里和后端各自为政。到最后权限码对不上,前端判断失效,后端也不认。

我自己的习惯是统一按模块:子模块:操作的三段式命名,比如:

  • cms:article:add
  • cms:article:edit
  • cms:article:audit
  • system:user:resetPwd

这段字符串同时作为前端指令参数和后端接口校验标识。建议在项目初期就维护一份权限码清单表,前后端共用,谁改谁同步,不然后面排查起来真的很痛苦。

4.4 刷新页面之后动态路由丢失的完整解法

这个问题在动态路由方案里属于必踩的坑。页面一按F5,vuex数据清空,动态addRoute加进去的路由也全部消失。可以利用路由守卫循环等待路由就绪的方式,也可以把静态路由和动态路由结合处理。

比较稳定的一套解法分三步:

  1. 在路由守卫里判断用户是否已登录;
  2. 已登录但vuex中没有权限数据时,先拉取权限数据并addRoute,然后next({ ...to, replace: true });
  3. 因为next重新进入了目标路由,此时to.matched已经存在,不会匹配到404。

首页白屏的问题基本能解决,但要注意动态路由必须加在静态通配符路由/:pathMatch(.*)*之前。

4.5 CMS富文本编辑器与内容的XSS过滤

CMS模块离不开富文本编辑器,推荐用wangEditor或者Quill这类成熟方案。但富文本编辑器的核心风险在于XSS,用户可以在内容区插入恶意脚本,如果原样存储、原样渲染,问题会很严重。

后端必须对富文本内容做白名单过滤,只允许p、img、a、ul、ol、li、strong、em、h2-h4这类安全标签。属性方面,建议只保留src、alt、href、title这几个常见属性。图片上传时也要校验文件类型和大小,防止恶意脚本伪装成图片上传。

个人强烈建议:不要在纯前端层面过滤,因为绕过前端请求直接POST后端接口,前端的任何过滤都不生效。完成后端过滤,并且对输出到页面的内容做一次兜底转义,双重保险。

5. 数据权限和角色设计里的几个容易忽视的边界

很多项目把“权限管理”理解成“菜单权限”,实现了菜单和按钮的控制就觉得权限系统完成了。但真实业务中,数据范围的权限往往更重要,甚至更能体现出系统的能力。

5.1 数据范围:编辑只能看到自己写的文章

CMS里很典型的一个需求:普通编辑只能看到自己创建的文章,主编能看到整个栏目的文章,管理员能看到所有部门的内容。这就是数据权限,不是菜单和按钮能解决的。

实现上通常做法是SQL层面拼接数据范围条件。角色表里加一个data_scope字段:

枚举值含义拼接条件
1仅本人数据author_id = 当前用户
2本部门数据dept_id in 当前用户所属部门及子部门
3全部数据不加条件

这个字段的存在会让查询方法看起来没那么干净,但它是控制数据越权最直接的手段。我曾经在一个项目里漏掉了数据权限,结果运营在后台看到并修改了其他城市同事的文章,非常尴尬。

5.2 超级管理员不要走常规权限判断

通常开发同学都会给“超级管理员”一个通配符角色,对应权限码*:*:*,判断时如果角色是超管就直接放行。这个设计问题不大,但要注意别在业务层写死if (username === 'admin')这种判断,正确做法是在底层权限框架里统一处理,业务代码不感知角色差异。

这样做的目的是保证权限策略可扩展,以后新加一个“运营总监”角色,即使也拥有全部权限,也不需要改代码。

5.3 权限变更后的实时生效策略

一个系统上线后,运维给某个人改了角色、或者调整了某个菜单的权限,用户那边需要什么操作才能生效?有些系统的做法是用户重新登录。在权限要求不是特别严苛的场景下,这也可以接受,但体验确实差了一些。

更友好的做法是:在后端支持“踢下线”功能,权限变更时通过WebSocket或者下一次请求时让用户强制刷新权限缓存。这个功能不一定放在第一版,但架构上要留好口子,至少把获取权限的接口独立出来,方便后续平滑升级。

6. 部署上线前的检查清单与性能优化

开发完毕并不代表可以上线。我每次负责后台项目上线前,都会按一套固定清单过一遍,免得在关键时刻掉链子。

6.1 接口访问日志与操作审计

后台系统是核心业务系统,谁在什么时间干了什么操作,必须能查。除了登录日志,还要有关键操作的操作日志。

日志不需要全量记录所有读接口,但写操作必须记录。最简单的方案是后端AOP切面,给需要审计的接口打上@OperationLog注解,切面里记录操作人、操作类型、参数、IP、耗时、结果。CMS的发布、下线、删除操作尤其要记,不然出问题没有回溯手段。

6.2 前端构建体积优化

Element Admin带上Element UI完整组件库,打包体积极容易膨胀。做构建优化时有几个直接见效的手段:

  • 开启组件按需引入,用babel-plugin-component把用不到的表单、日期选择器、弹窗组件全部摇掉;
  • 路由懒加载,每个页面单独分包,而不是把全部页面打进一个chunk里;
  • 关闭源码映射,生产环境的productionSourceMap设为false,体积能小很多;
  • 开启Gzip,配合Nginx的gzip_static,对整体传输体积的优化非常明显。

经验来看,做完这几步,后台首页从首次打开三四秒能压到一秒半左右,感官提升还是很明显的。

6.3 部署时的环境变量管理

环境变量别写在代码里。Element Admin配套的Vue.config.js里通常会区分development、test、production三个环境,API地址、上传地址这些按环境取值。

有一点容易踩坑的是,前端在构建时把环境变量打进了静态资源里,所以换环境必须重新构建,不能直接把构建好的dist包拿到另一个环境部署。如果要做多环境共用同一个包,就需要运行时挂载全局配置,可以配合Nginx注入或使用env.js动态加载方式。

6.4 Nginx需要单独处理的前端路由问题

使用History模式路由时,Nginx必须配置try_files回退到index.html,否则直接访问二级路径会报404:

location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }

另外,如果前端静态资源请求用的是相对路径,在部署到子目录时需要设置base和publicPath,否则CSS和JS全部加载失败。这种问题通常只在部署当天才暴露,提前check一下能省很多时间。

7. 从我自己的项目经验出发,最后想说的几件事

这个组合方案我先后在三个不同类型的项目里落地过,一个偏企业信息门户、一个偏内容社区、还有一个是内部管理工具,效果都比较稳定。ElementAdmin作为底子,权限系统提供访问控制,CMS模块负责内容生产,两者共用一个后台,数据和用户体系天然统一。

有一点我体会最深:权限和CMS从来不是两个孤立的功能模块,它们的耦合点也绝不只是侧边栏菜单,而是贯穿了用户、角色、内容、状态、审核、审计的全流程。设计时如果分开做,后面接起来必然要返工。

如果你正准备用这个方案启动自己的项目,我的建议很直接:第一版不要贪多,先把用户登录、动态菜单、文章发布、状态流转、操作日志这几个主链路跑通,权限码和数据权限的模型想清楚再动手写。过程里遇到问题不要急着怀疑模板不行,大部分问题都是动态路由时序、权限数据同步和状态管理这三件事没有理顺导致的。

按照上文梳理的链路一步一个脚印走,你会发现自己搭出来的后台既干净又扛得住需求变化,这大概就是这个方案最让人省心的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询