接手过那么多管理后台项目,我一直有个很深的感触:权限管理和内容管理看着是两件事,但在真实业务里它们总是绑在一起出现。后台这边要控制谁能进哪个菜单、谁能点哪个按钮,前台那边要有人发文章、传图片、管理分类,两边还共用一套账号体系。所以当团队决定用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 | 父级菜单ID | 0表示顶级 |
| 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封面图URLcontent正文内容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:addcms:article:editcms:article:auditsystem:user:resetPwd
这段字符串同时作为前端指令参数和后端接口校验标识。建议在项目初期就维护一份权限码清单表,前后端共用,谁改谁同步,不然后面排查起来真的很痛苦。
4.4 刷新页面之后动态路由丢失的完整解法
这个问题在动态路由方案里属于必踩的坑。页面一按F5,vuex数据清空,动态addRoute加进去的路由也全部消失。可以利用路由守卫循环等待路由就绪的方式,也可以把静态路由和动态路由结合处理。
比较稳定的一套解法分三步:
- 在路由守卫里判断用户是否已登录;
- 已登录但vuex中没有权限数据时,先拉取权限数据并addRoute,然后
next({ ...to, replace: true }); - 因为
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从来不是两个孤立的功能模块,它们的耦合点也绝不只是侧边栏菜单,而是贯穿了用户、角色、内容、状态、审核、审计的全流程。设计时如果分开做,后面接起来必然要返工。
如果你正准备用这个方案启动自己的项目,我的建议很直接:第一版不要贪多,先把用户登录、动态菜单、文章发布、状态流转、操作日志这几个主链路跑通,权限码和数据权限的模型想清楚再动手写。过程里遇到问题不要急着怀疑模板不行,大部分问题都是动态路由时序、权限数据同步和状态管理这三件事没有理顺导致的。
按照上文梳理的链路一步一个脚印走,你会发现自己搭出来的后台既干净又扛得住需求变化,这大概就是这个方案最让人省心的地方。