ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TP6+Layui RBAC权限系统实战:三表设计与按钮级控制

TP6+Layui RBAC权限系统实战:三表设计与按钮级控制 简介本资源是一套基于Layui 2.5.6与ThinkPHP 6.0.2深度集成的权限管理后台完整工程面向PHP中级开发者及Web全栈学习者解决企业级后台系统中角色划分、菜单控制与接口权限校验等核心问题。压缩包共1098个文件涵盖481个PHP后端逻辑文件、139个JS前端交互脚本、94个HTML页面模板、30个CSS样式文件及120个GIF/PNG图标资源整体体积6.1MB结构清晰含.env环境配置、composer依赖管理、RBAC权限模型实现及public静态资源目录等典型TP6工程要素。已有2283人学习下载资源开箱即用提供从数据库初始化、登录鉴权、菜单动态渲染到操作日志记录的全流程功能闭环同时附带README说明、LICENSE协议及CI配置.travis.yml便于二次开发与教学复现。1. 这不是又一个“后台模板”而是能直接跑通 RBAC 权限流的最小可运行闭环Layui 2.5.6 ThinkPHP 6.0.2 组合落地实录你花两小时配好 Layui 表单、写完 TP6 路由、建好五张权限表结果点击「用户管理」跳转 404点「角色分配」报Undefined method: hasRole()——这不是玄学是绝大多数人卡在「权限管理后台」落地第一关的真实翻车现场。这个资源包不是 UI 套壳也不是空架子 Demo它是一套经过生产环境简化验证的 RBAC 实现体从 MySQL 的auth_rule/auth_role/auth_user_role三张核心表结构到 TP6 中间件级权限拦截非控制器注解、Layui 数据表格与弹窗表单的双向绑定逻辑再到菜单动态渲染与按钮级显隐控制全部用原生 TP6 模型 Layui 原生 JS 实现零依赖第三方扩展包。适合正在用 TP6 搭建内部系统、需要快速交付权限模块的后端工程师或全栈开发者——尤其当你已经写完业务逻辑却卡在「怎么让张三只能看订单李四能删但不能改」这个具体问题上时这份资源不是参考是能直接git clone composer install php think migrate:run启动、改两行配置就能进后台调试的黑匣子。2. 从数据库到路由RBAC 三张表设计与 TP6 路由分组的硬约束对齐2.1 核心数据表结构为什么只用三张表而不是五张RBAC 模型常被讲成「用户-角色-权限-资源-操作」五元组但在 TP6 Layui 这个轻量级组合里过度拆分反而增加联查复杂度和前端渲染负担。本资源采用精简但可扩展的三表结构表名字段关键说明auth_ruleid,name,title,type(menu/button),status,pid,sort,icon,url所有可控制的权限节点菜单项typemenu如「用户管理」按钮项typebutton如「删除」url存完整路径/admin/user/delete供中间件比对auth_roleid,name,status,remark角色定义如「管理员」「审核员」status控制启用/禁用auth_user_roleuser_id,role_id用户与角色多对多关系不存权限只存角色归属——权限继承通过角色关联规则实现提示auth_rule表中url字段必须与 TP6 路由定义完全一致含前缀例如路由定义为Route::get(admin/user/list, UserControllerlist)则url必须填/admin/user/list否则中间件无法匹配。这是后续权限拦截失效的头号原因。2.2 TP6 路由分组与命名空间让权限校验能精准命中控制器方法TP6 的路由分组机制是权限控制的基石。本资源强制要求所有后台接口统一注册在admin分组下并启用prefix和middleware// app/route/app.php use think\facade\Route; Route::group(admin, function () { // 系统管理 Route::get(index, IndexControllerindex); Route::get(user/list, UserControllerlist); Route::post(user/add, UserControlleradd); Route::post(user/delete, UserControllerdelete); // 权限管理 Route::get(role/list, RoleControllerlist); Route::post(role/assign, RoleControllerassign); })-prefix(app\\controller\\admin\\)-middleware(auth);关键点在于-prefix(app\\controller\\admin\\)确保所有路由指向app/controller/admin/下的控制器避免因命名空间错位导致AuthMiddleware无法反射获取方法-middleware(auth)全局挂载自定义权限中间件该中间件会解析当前请求 URL如/admin/user/delete查询登录用户所属角色拥有的所有auth_rule.url进行精确字符串匹配非模糊匹配防止/user匹配到/user/export。2.3 Layui 菜单渲染逻辑如何把auth_rule的树形结构变成左侧导航栏Layui 本身不提供菜单组件本资源用原生 JS Layuitree模块实现动态菜单。后端返回的数据结构必须严格满足[ { id: 1, title: 系统管理, icon: layui-icon-set-fill, spread: true, children: [ { id: 2, title: 用户管理, icon: layui-icon-user, href: /admin/user/list } ] } ]后端IndexControllerindex方法中通过递归查询auth_rule构建此结构// app/controller/admin/IndexController.php public function index() { $menu $this-buildMenuTree(); return view(, [menu json_encode($menu)]); } private function buildMenuTree($pid 0) { $rules Db::name(auth_rule) -where([pid $pid, type menu, status 1]) -order(sort ASC) -select(); foreach ($rules as $rule) { $rule[children] $this-buildMenuTree($rule[id]); unset($rule[pid], $rule[type], $rule[url]); // 清理冗余字段 } return $rules; }注意href字段值必须是完整路径如/admin/user/list且与路由定义中的url完全一致Layuitree组件才能正确跳转。3. 权限拦截与按钮控制TP6 中间件 Layui 按钮显隐的双重保险3.1 自定义 AuthMiddleware基于 URL 的精准拦截拒绝“一刀切”TP6 默认中间件是闭包形式本资源封装为类便于复用与调试// app/middleware/Auth.php ?php declare (strict_types 1); namespace app\middleware; use think\facade\Db; use think\facade\Session; use think\Response; class Auth { public function handle($request, \Closure $next) { // 未登录跳转登录页 if (!Session::has(admin_user_id)) { return redirect(/login); } $user_id Session::get(admin_user_id); $current_url parse_url($request-url(), PHP_URL_PATH); // 查询用户所有可访问的 URL 列表 $allowed_urls Db::name(auth_rule) -alias(r) -join(auth_user_role ur, ur.role_id r.role_id) -join(auth_role ro, ro.id ur.role_id AND ro.status 1) -where(ur.user_id, $user_id) -where(r.type, menu) -where(r.status, 1) -column(r.url); // 允许访问注意此处是精确匹配非前缀匹配 if (!in_array($current_url, $allowed_urls)) { // 检查是否为按钮级权限typebutton $button_urls Db::name(auth_rule) -alias(r) -join(auth_user_role ur, ur.role_id r.role_id) -join(auth_role ro, ro.id ur.role_id AND ro.status 1) -where(ur.user_id, $user_id) -where(r.type, button) -where(r.status, 1) -column(r.url); if (!in_array($current_url, $button_urls)) { return Response::create(无权限访问, html, 403)-header([X-Frame-Options DENY]); } } return $next($request); } }逻辑说明parse_url($request-url(), PHP_URL_PATH)提取纯路径剥离 GET 参数避免/user/list?page1与/user/list不匹配先查typemenu的菜单权限再查typebutton的按钮权限两者分离互不干扰返回 403 响应并设置X-Frame-Options头防止被嵌入 iframe 攻击。3.2 Layui 表格内按钮显隐用templet>// 前端 JS用户列表页 table.render({ elem: #userTable, url: /admin/user/list, cols: [[ {field: id, title: ID, width: 80}, {field: username, title: 用户名}, {field: email, title: 邮箱}, {title: 操作, width: 180, align: center, toolbar: #userBar} ]], page: true }); // 工具栏模板 script typetext/html iduserBar {{# if(d.layuiBtns d.layuiBtns.indexOf(edit) -1){ }} a classlayui-btn layui-btn-xs lay-eventedit编辑/a {{# } }} {{# if(d.layuiBtns d.layuiBtns.indexOf(delete) -1){ }} a classlayui-btn layui-btn-danger layui-btn-xs lay-eventdel删除/a {{# } }} /script后端UserControllerlist在返回 JSON 数据时为每条记录注入layuiBtns字段// app/controller/admin/UserController.php public function list() { $user_id Session::get(admin_user_id); // 查询当前用户拥有的按钮权限 $btn_urls Db::name(auth_rule) -alias(r) -join(auth_user_role ur, ur.role_id r.role_id) -join(auth_role ro, ro.id ur.role_id AND ro.status 1) -where(ur.user_id, $user_id) -where(r.type, button) -where(r.status, 1) -column(r.url); // 提取按钮标识/admin/user/edit → edit/admin/user/delete → delete $btns []; foreach ($btn_urls as $url) { $parts explode(/, trim($url, /)); if (count($parts) 3) { $btns[] end($parts); // 取最后一段作为按钮 key } } $users Db::name(admin_user)-select(); foreach ($users as $user) { $user[layuiBtns] $btns; // 注入到每条数据 } return json([code 0, msg , data $users]); }参数说明d.layuiBtns是 Layui 模板变量对应后端传入的layuiBtns数组indexOf(edit) -1判断当前用户是否拥有edit按钮权限避免 DOM 渲染后 JS 再做判断减少前端逻辑耦合按钮 URL 设计为/admin/user/edit后端自动提取edit作为 key符合 RESTful 习惯且易于维护。4. 避坑指南TP6 Layui 权限模块落地中最常踩的五个坑4.1 现象菜单显示正常但点击任意菜单都跳转到首页原因auth_rule.url字段值与 TP6 路由定义的路径不一致。常见错误包括路由定义为Route::get(user/list, ...)但url填了/user/list缺admin/前缀或路由启用了suffix如.html但url未加后缀。解决检查app/route/app.php中路由定义的完整路径确保auth_rule.url与其完全一致包括前缀、斜杠、后缀。用dd($request-url())在中间件中打印实际请求路径对比。4.2 现象登录后能打开菜单但所有按钮都不可见原因auth_rule.type字段值错误。按钮权限必须设为button菜单权限为menu。若误将按钮也设为menu则buildMenuTree()会将其纳入菜单树而按钮显隐逻辑只查typebutton的记录。解决执行 SQL 检查SELECT * FROM auth_rule WHERE type NOT IN (menu, button);修正错误类型。新增按钮时务必手动设置typebutton。4.3 现象角色分配后用户权限不生效需重新登录才更新原因权限缓存未清除。TP6 默认不缓存权限数据但若你在AuthMiddleware中使用了Cache::get()且未设置过期时间或 Session 中admin_user_id缓存了旧角色数据。解决在RoleControllerassign方法末尾强制清除用户权限缓存Cache::delete(auth_rules_ . $user_id); Session::delete(auth_rules_ . $user_id);并在AuthMiddleware中优先读取 Session 缓存未命中再查 DB 并写入 Session。4.4 现象Layui 表格toolbar模板中d.layuiBtns报undefined原因后端返回的 JSON 数据中layuiBtns字段未注入到每条记录的数组中而是作为顶层字段返回如{code:0, data:[...], layuiBtns:[edit]}导致模板无法访问。解决确认UserControllerlist中foreach ($users as $user)循环已为每个$user数组添加layuiBtns $btns而非只在顶层添加。4.5 现象AuthMiddleware中in_array($current_url, $allowed_urls)始终为 false原因$allowed_urls是二维数组或对象数组而非一维字符串数组。常见于Db::name()-column()未指定字段名返回的是[url /xxx]形式。解决column()必须指定字段名-column(r.url)而非-column(url)或-column()。打印dump($allowed_urls)确认其结构为[/admin/user/list, /admin/role/list]。5. 按钮级权限的进阶控制从「显隐」到「禁用文案提示」的三层防御5.1 为什么仅靠显隐不够真实业务场景的三个反例审计合规要求财务审批流程中「驳回」按钮必须存在但置灰且 hover 时显示「当前状态不可驳回」而非直接隐藏——隐藏等于否认操作可能性审计日志无法追溯意图用户体验断层用户看到「导出」按钮消失会疑惑「功能没了」而置灰提示能明确传达「你有权限但此刻条件不满足」前端安全补位即使后端拦截了/export请求若按钮完全消失攻击者仍可通过构造 URL 直接访问置灰按钮配合前端 JS 校验形成第二道防线。5.2 Layui 按钮状态控制用lay-event>script typetext/html iduserBar {{# if(d.btnStatus d.btnStatus.edit disabled){ }} a classlayui-btn layui-btn-xs layui-disabled lay-eventedit>// 根据用户权限 当前数据状态动态计算按钮状态 foreach ($users as $user) { $user[btnStatus] [ edit $this-canEdit($user) ? enabled : disabled, delete $this-canDelete($user) ? enabled : disabled ]; } private function canEdit($user) { // 1. 检查用户是否有 edit 权限 $hasEditPerm in_array(/admin/user/edit, $this-getUserButtonUrls()); // 2. 检查业务规则禁止编辑超级管理员 return $hasEditPerm $user[is_super] ! 1; } private function canDelete($user) { $hasDelPerm in_array(/admin/user/delete, $this-getUserButtonUrls()); // 3. 业务规则禁止删除自己 $current_user_id Session::get(admin_user_id); return $hasDelPerm $user[id] ! $current_user_id; }5.3 Hover 提示与点击拦截用 Layuitooltipevent.preventDefault()为禁用按钮添加 Tooltip并阻止点击// 初始化后执行 table.on(tool(userTable), function(obj){ var data obj.data; var event obj.event; if (obj.tr.find([data-statusdisabled]).length 0) { // 禁用按钮点击拦截 obj.tr.find([data-statusdisabled]).on(click, function(e){ e.preventDefault(); layer.msg(当前操作不可用 $(this).attr(title), {icon: 0}); }); } if(event edit){ // 正常编辑逻辑 } else if(event del){ // 正常删除逻辑 } }); // 为禁用按钮添加 Tooltip $(.layui-disabled).each(function(){ var $btn $(this); var title $btn.text() 编辑 ? 您无权编辑此用户 : 您无权删除此用户; $btn.attr(title, title); layui.use(tooltip, function(){ layui.tooltip.render({ elem: $btn[0], content: title, tips: 3 }); }); });注意layui-disabled是 Layui 内置 CSS 类会自动添加灰色背景与pointer-events: none但为兼容性仍需e.preventDefault()双重保险。从那以后我每次交付权限模块都会在auth_rule表里多加一列business_ruleTEXT存 PHP 表达式如$user[status] 1 $data[is_locked] 0然后在canEdit()里eval()执行——不是为了炫技而是让产品同学能直接改规则不用每次提需求都找我改代码。这套 TP6 Layui 的 RBAC 实现真正价值不在“能跑”而在“改起来不撕裂”。希望帮到你。本文还有配套的精品资源点击获取
返回列表