Layui按钮权限控制实战:从权限码设计到表格工具栏处理与踩坑记录
引子一次权限需求让我在layui里折腾了两天现在的后台管理系统权限控制基本是标配。大部分项目做到菜单级、路由级权限就算交差但真到了业务方手里他们还会提一个很具体的要求“这个删除按钮只有管理员能看到运营只能看不能删”。这就是按钮权限控制。我前段时间接了一个老项目前端用的layui后台是Java页面全是服务端模板加layui的模块化加载方式。项目已经跑了一年多菜单权限早做好了但所有操作按钮都裸奔在页面上只要有菜单权限谁都能点删除、点审核。业务那边出新规要求按钮按角色区分没有权限的按钮要么不渲染要么渲染了也要禁用。我在动手前查了些资料顺便看了下大家对layui的讨论发现layui date组件怎么限制最大日期为当前日期、layui select怎么动态赋值这类问题也经常被翻出来说明很多人在做layui后台时都在同一个战场里挣扎。今天就把我在老项目里给layui增加按钮权限控制的完整思路、踩坑记录和可直接抄的代码分享出来希望能帮你少走点弯路。需要说明的是权限控制这件事前端做的只是“体验层控制”真正的安全底线一定在后端接口校验。前端按钮隐藏得再漂亮后端接口不设防等于白干。这一点后面我会再强调。1. 权限控制整体设计思路1.1 到底控制哪些按钮在很多后台里按钮权限的粒度其实很清晰基本围绕“增删改查”展开新增、编辑、删除、审核、导出、导入、发布、撤回、重置密码、分配角色。每个按钮对应后端一个接口权限码也叫权限标识前后端共用一套编码。我在做之前先梳理了项目的页面清单把每个页面的操作按钮全列出来然后和业务方逐一对齐整理成了一张权限点表。比如用户管理页面就有“用户新增”“用户编辑”“用户删除”“重置密码”“分配角色”这五项。整理这张表很关键因为它是后端设计权限码的基础也是前端写按钮判断条件的依据。1.2 思路选型后端渲染还是前端判断在layui这种服务端渲染页面占主导的项目里有几种常见的按钮权限实现方式后端模板引擎直接判断有权限才渲染按钮。这种最“硬”但侵入性强每个按钮都要写一遍if判断页面模板会变得很臃肿。前端页面加载后拉取权限数据统一做按钮的隐藏或禁用。这种更适合layui这种前端交互比较重的项目改动集中在公共JS里维护方便。前端只控制显隐后端接口做二次校验。这是安全底线必须做。我选的是第二种加第三种结合。前端用layui的模块化机制在页面初始化时拿权限集合然后通过公共函数统一控制按钮。这样既能快速适配老页面也不影响模板的可读性。注意后端接口校验是必须的前端隐藏按钮只是提升体验、防止误操作不是安全手段。别指望靠前端隐藏来阻止攻击者。1.3 权限数据的存储与获取在现有项目里菜单权限是登录后由后端返回存在session里的。按钮权限我沿用同一套机制登录接口返回用户的按钮权限码列表前端存到全局变量里。权限码的格式建议用“模块:操作”比如user:add、user:delete、order:export。这种格式可读性好后端做权限拦截时也好统一匹配。数据返回的格式我将后端接口设计成下面这种结构{ code: 0, msg: success, data: { userId: 1001, userName: zhangsan, buttons: [ user:add, user:edit, user:delete, role:assign ] } }前端拿到这个列表后放到一个全局数组里比如window.PERMISSION_BUTTONS。这个数组就是后面所有判断的“依据”。没有在数组里的权限码对应按钮就不渲染或者禁用。1.4 这套方案的优点和局限优点是改造量小。老项目里的按钮大部分是写死在HTML里的layui按钮少部分是通过模板动态渲染的。我只需要在公共JS里加一个权限校验函数再把需要控制的按钮加上自定义属性比如>PostMapping(/login) public Result login(RequestBody LoginDTO dto) { // 1. 用户认证 User user userService.login(dto.getUsername(), dto.getPassword()); if (user null) { return Result.error(用户名或密码错误); } // 2. 获取按钮权限码 ListString buttonPerms permissionService.getUserButtonPerms(user.getId()); // 3. 组装返回结果 LoginVO vo new LoginVO(); vo.setUserId(user.getId()); vo.setUserName(user.getUsername()); vo.setButtons(buttonPerms); return Result.success(vo); }这段代码的逻辑很直白用户认证通过后根据用户ID去关联查询权限码列表最后封装到登录返回体里。前端拿到的buttons数组就是后续按钮控制的数据源。2.3 为什么建议共用接口而不是新增接口可能有人会说登录接口已经很大了再塞一个按钮权限列表进去有点乱。我的考虑是按钮权限和菜单权限一样都是用户登录后的“基础数据”一次请求全部拿到前端缓存好后续不需要再发额外请求。如果单独开一个接口前端每次进页面都要异步请求一次既增加请求量又容易遇到时序问题——权限数据还没回来按钮已经渲染完了。当然如果你的项目里按钮权限数据量特别大或者需要在某些场景下动态刷新权限也可以单独拉取。但大多数后台项目登录时一次拿全是最省心的方案。3. 前端核心实现按钮权限校验3.1 初始化时保存权限数据在前端我在入口页面上统一处理登录返回的数据。因为老项目用的是layui没有Vue那种全局状态管理我就直接挂window对象上// login.js var loginRes res.data; window.PERMISSION_BUTTONS loginRes.buttons || []; // 提供一个全局函数判断是否有权限 window.hasPerm function (perm) { if (!perm) { return true; } return window.PERMISSION_BUTTONS.indexOf(perm) ! -1; };这样每个页面都能通过hasPerm(user:delete)这种方式来判断。判断结果是true就显示或启用按钮false就隐藏或禁用。这里有个小细节indexOf在低版本浏览器下没问题因为layui项目通常兼容性做得不错不支持includes的话就用indexOf稳妥起见我一直用indexOf。3.2 三种按钮控制方式隐藏、禁用、只读按钮权限控制在视觉层面一般有三种表现直接隐藏没有权限按钮不显示。这种最干净用户根本看不到入口适合删除、导出这种比较敏感的操作。禁用置灰按钮还在但点了没反应。适合审查类按钮让用户知道有这个功能但自己没权限。禁用通常还要加个tooltip提示比如“需要管理员权限”。只读适合详情、查看这种操作其实不需要做权限控制但如果业务要求严格也可以控制。在layui里隐藏和禁用都有对应的实现。隐藏可以用jQuery的hide()或直接不渲染禁用可以给按钮加layui-disabled类或者设置disabled属性。3.3 页面初始按钮的权限处理在静态页面里按钮一般是写死在HTML里的。我给这些按钮加一个自定义属性>button typebutton classlayui-btn>// common.js function handleBtnPermission() { // 遍历所有带>table.render({ elem: #userTable, url: /user/list, cols: [[ { field: id, title: ID }, { field: username, title: 用户名 }, { title: 操作, toolbar: #userTableToolbar, width: 200 } ]] });#userTableToolbar是script模板script typetext/html iduserTableToolbar button classlayui-btn layui-btn-xs lay-eventedit>table.render({ // ... done: function (res) { handleBtnPermission(); } });这个方案问题在于一旦表格有分页、刷新done回调会再次执行按钮会被重复remove。如果第一次进来权限是false按钮已经remove了翻页后重新渲染done里会再次remove其实没问题但逻辑上不够优雅。办法二在模板渲染前过滤。因为laytpl模板就是一段HTML字符串你可以在table.render之前先把模板内容读出来根据权限把没权限的按钮从模板字符串里替换掉然后再传给table。// 读取模板 HTML var tplHtml $(#userTableToolbar).html(); // 定义权限校验的替换逻辑 var reg /button[^]*data-perm([^]*)[^]*[\s\S]*?\/button/g; tplHtml tplHtml.replace(reg, function (match, perm) { if (!window.hasPerm(perm)) { return ; // 没有权限移除整个button } return match; }); // 再用处理过的模板渲染表格 table.render({ elem: #userTable, url: /user/list, toolbar: , cols: [[ { title: 操作, toolbar: tplHtml, width: 200 } ]] });这种方式的好处是在模板字符串阶段就把按钮过滤掉了表格渲染出来就是干净的。缺点是如果模板里有复杂的嵌套正则写起来要小心。不过对大多数后台项目的简单button模板来说这个正则足够用了。3.5 按钮点击事件的权限二次校验隐藏按钮是第一步但还要考虑一种情况用户通过浏览器控制台手动把按钮的display改回来或者直接调用按钮绑定的click事件。如果前端函数体里没有做权限判断这时候操作就能触发。所以最好在关键操作的点击事件里再加一次权限校验$(#delUserBtn).on(click, function () { var perm $(this).attr(data-perm); if (!window.hasPerm(perm)) { layer.msg(抱歉您没有操作权限); return; } // 执行删除逻辑 });这样即使按钮被“复活”点击事件也会被拦截。虽然这种防御更多是心理安慰——真正懂技术的直接调接口去了——但写下来成本不高也能防止一些误操作。4. 在真实项目里落地结合select动态赋值和日期控件4.1 一个典型用户管理页面的完整改造我拿用户管理页来说。这个页面有搜索区、表格、工具栏按钮还有新增/编辑弹窗。改造前页面上有新增、编辑、删除、导出、重置密码五个按钮全部可见。改造后的步骤第一步在HTML里给需要控制的按钮加上>layer.open({ type: 1, content: $(#editFormTpl).html(), success: function (layero) { // 弹窗DOM挂载完成后扫描按钮权限 handleBtnPermissionInLayer(layero); } });handleBtnPermissionInLayer的写法跟handleBtnPermission类似只是把选择器范围限定在弹窗DOM内function handleBtnPermissionInLayer(layero) { $(layero).find([data-perm]).each(function () { var permCode $(this).attr(data-perm); if (!window.hasPerm(permCode)) { $(this).remove(); } }); }4.3 配合layui select动态赋值的权限场景权限控制经常和表单动态赋值搅在一起。比如编辑用户时角色下拉框要根据当前用户的权限来动态赋值——如果当前登录用户没有“分配角色”权限那么编辑弹窗里的角色下拉框就应该禁用或者不显示。我遇到的一个具体场景是select里的选项根据按钮权限动态渲染。// 编辑用户弹窗打开时 function openEditDialog(row) { var perm user:assignRole; layer.open({ type: 1, title: 编辑用户, content: $(#editUserFormTpl).html(), success: function (layero) { // 初始化表单 form.val(editUserForm, { username: row.username, role: row.roleId }); // 如果没有分配角色权限禁用角色下拉框 if (!window.hasPerm(perm)) { var roleSelect $(layero).find(select[namerole]); roleSelect.attr(disabled, disabled); // 重新渲染select form.render(select); } } }); }这里有个关键点类select禁用后要调用form.render(select)重新渲染layui的select是经过封装的自定义组件直接改DOM属性不会生效必须重新渲染。4.4 与layui date最大日期控制的联动有一个场景我印象很深数据导出时需要选择日期范围业务要求导出结束日期不能超过当前日期。这个逻辑本身和权限无关但和权限控制叠加后会出现一个很微妙的问题——如果用户没有某个月的导出权限那么即使日期范围合法导出按钮也应被禁用。我在做权限控制后把日期限制和权限判断写在了同一个按钮点击事件里// 导出按钮点击 $(#exportBtn).on(click, function () { // 权限判断优先 var perm $(this).attr(data-perm); if (!window.hasPerm(perm)) { layer.msg(没有导出权限); return; } // 日期范围判断 var dateRange $(#dateRange).val(); if (!dateRange) { layer.msg(请选择日期范围); return; } var endDate dateRange.split( - )[1]; var today getToday(); // 例如 2025-01-15 if (endDate today) { layer.msg(结束日期不能超过今天); return; } // 执行导出 exportData(dateRange); });这种把权限判断、日期校验都放在一起的写法让事件处理逻辑更集中后续排查问题时也方便。我还在日期控件初始化时直接限制了可选范围laydate.render({ elem: #dateRange, type: date, range: true, max: getToday(), // 最大日期为当前日期 min: 2020-01-01 });通过max参数直接限制最大日期为当前日期用户就没法在界面上选到未来日期了。这个参数也是layui date组件里非常常用的一个配置配合权限控制一起用能少写很多业务判断。4.5 layui和Vue共存时的按钮权限处理很多人问“layui可以用vue吗”答案是能用但要看场景。我这边用的纯layui没引Vue。如果你在一个项目里同时用了Vue和layui按钮权限控制的方式会略有不同。Vue负责视图渲染时按钮通常写在Vue模板里这时可以直接用v-if指令配合hasPerm函数button v-ifhasPerm(user:add) classlayui-btn clickhandleAdd 新增用户 /button如果hasPerm是挂载在window上的全局函数Vue实例里可以直接在methods里包一层methods: { hasPerm(perm) { return window.hasPerm(perm); } }模板里就能直接用了。这种组合方式的好处是Vue的响应式系统会自动更新视图不需要手动扫描DOM。前提是权限数据要在Vue实例创建之前就拿到否则模板渲染时hasPerm返回false按钮就不会显示。如果你用的是Vue 3的script setup语法可以用computed把权限列表包装一下然后到处引用。但本质思路不变控制按钮显隐的条件就是权限列表里有没有对应的权限码。5. 常见问题与排查技巧实录5.1 表格渲染后权限按钮又出现了我最初用done回调扫描DOM时遇到过一个问题表格数据刷新后没权限的按钮闪了一下才消失。原因是表格渲染是异步的底层渲染完done回调触发但浏览器还没把DOM绘制到屏幕上时我的代码已经把按钮移除了。逻辑上没问题但视觉上有闪烁感。后来我改用模板字符串预过滤的方案这个问题就彻底消失了。因为按钮在渲染前就不存在不存在“先显示再隐藏”的问题。5.2 权限码改了按钮还是显示老的这种情况基本是缓存问题。如果权限数据是存在localStorage或sessionStorage里的修改后端权限后用户需要重新登录才能拉取新的权限列表。我在项目里加了一个小机制登录接口返回的用户信息里带一个权限版本号前端存起来。每次打开页面时如果版本号变了就强制重新拉取权限数据。不过这个机制有点重大多数项目不需要。简单粗暴的办法就是让用户重新登录或者前端在检测到页面打开时间超过一定时长后主动刷新权限。5.3 批量按钮remove后表格复选框列还需要吗这个前面提了一嘴。我的建议是如果批量操作按钮全部被移除那么表格的复选框列也应该不渲染。具体做法是在cols配置里根据权限动态组装var checkField window.hasPerm(user:batch) ? { type: checkbox } : null; var cols []; if (checkField) { cols.push(checkField); } cols.push({ field: id, title: ID }); // ...在table.render之前组装好cols数组避免复选框出现后用户勾选了但没有任何可执行的操作这会让用户很困惑。5.4 权限按钮和layui事件绑定冲突一个容易踩的坑是在table.on(tool(...))事件里lay-event才是判断按钮行为的依据。如果你把按钮移除了那么用户点击不到它事件自然也不会触发。但有一种情况要注意如果你用的是事件委托而且移除按钮后重新插入了一个相同ID的按钮那么老的事件绑定可能还在。这种情况多发生在按钮被remove后又在其他地方被重新插入时。解决办法是尽量用lay-event区分按钮功能避免用按钮ID绑定事件。layui的表格工具栏事件本身就是基于lay-event的所以这个坑在表格工具栏里反而不容易出现。问题多发在普通页面按钮上——如果你用$(#delBtn).on(click, ...)这种方式绑定事件按钮被移除后如果某处代码又把相同ID的按钮插入回来了那老的事件可能失效或者触发多次。我建议页面按钮的点击事件统一用类名加事件委托来绑定$(document).on(click, .perm-del-btn, function () { // ... });按钮加了perm-del-btn这个类被移除后再插入事件依然有效。5.5 测试时怎么验证权限控制最后讲讲测试。不能只测“有权限的账号能看到按钮没权限的账号看不到”这种最基础的情况。我一般会额外测试几组数据测试场景期望结果用户没有角色所有需要权限的按钮都应移除用户只有查看权限操作列除查看外全部不显示用户拥有全部权限所有按钮正常显示、可操作权限码错误或不存在的页面对应按钮被移除页面不报错直接通过控制台调用click事件事件内拦截弹出无权限提示前两项是比较容易忽略的边界情况。很多系统的按钮权限控制只做了“有权限时显示”没做“没权限时必须移除”结果没角色的用户登录后看到一堆按钮点啥都提示无权限体验很差。5.6 权限数据为空时的兜底策略还有一个细节也是我踩过的坑如果用户登录后后端因为某种原因没返回buttons字段前端拿到的就是undefined这时候hasPerm会怎么表现如果你写的是window.hasPerm function (perm) { return window.PERMISSION_BUTTONS.indexOf(perm) ! -1; };当PERMISSION_BUTTONS是undefined时会直接抛异常页面崩溃。所以初始化时一定要兜底window.PERMISSION_BUTTONS (loginRes.buttons || []);这是我遇到过最隐蔽的问题之一——接口正常返回但返回的buttons字段因为后端某些逻辑漏掉了结果前端直接白屏。兜底后即使权限数据为空也只是所有受控按钮都不显示页面主体功能不受影响。最后再分享两个小技巧第一个技巧在做按钮权限控制时建议在HTML注释里把权限码标注出来!-- perm: user:add -- button typebutton classlayui-btn>window.PERMS { USER_ADD: user:add, USER_EDIT: user:edit, USER_DELETE: user:delete, USER_EXPORT: user:export, USER_RESET_PWD: user:resetPwd };写判断的时候直接hasPerm(PERMS.USER_DELETE)避免手写字符串拼错。权限码一多字符串拼错的概率真不低而且这种错误很难在测试时一眼发现——往往是部署上线后某个按钮突然不见了排查半天才发现是权限码少了一个字母。我在实际项目中做完这次按钮权限改造后最大的体会是权限控制并不难难的是把权限体系和现有的业务代码理顺。尤其是老项目页面多、按钮杂、权限码命名不统一前期梳理工作往往比写代码更耗时。但这一步千万不能省理清楚了后面的控制逻辑就是顺水推舟的事。希望这篇文章能帮你在layui项目里少走弯路也欢迎你在实践中有更好的思路来找我交流。