ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Layui+ThinkPHP6 RBAC权限系统实战搭建指南

Layui+ThinkPHP6 RBAC权限系统实战搭建指南 简介这是一套基于Layui 2.5.6与ThinkPHP 6.0.2深度集成的权限管理后台实战项目面向PHP中初级开发者及企业级后台系统学习者解决前后端分离趋势下轻量级RBAC权限系统快速落地的痛点。资源包共1098个文件涵盖481个PHP后端逻辑文件、139个JS交互脚本、94个HTML模板页、30个CSS样式文件及120个GIF/PNG图标资源完整呈现TP6路由控制、中间件鉴权、Layui动态菜单渲染与数据表格联动等核心实现压缩包仅6.1MB结构清晰含.env环境配置、composer依赖管理、Travis持续集成及规范README说明。已有2283人学习下载开箱即用可直接部署调试亦便于拆解学习RBAC模型设计、Layui模块化开发、TP6多应用架构与前后端协同调试流程。1. 为什么用 Layui 2.5.6 搭配 ThinkPHP 6.0.2 做权限后台反而比“新潮框架”更稳、更快落地你手头有个中型内部管理系统要上线老板说“下周五前必须能跑通用户登录菜单权限控制”开发周期只有 7 天。这时候翻 GitHub 看 Vue3 Pinia Element Plus 的权限方案光配路由守卫、动态菜单、按钮级权限指令就得调两天——还未必兼容 IE11别笑很多政企内网真要。而我去年在三个交付项目里反复验证Layui 2.5.6 TP6.0.2 这套组合是 RBAC 权限管理后台的“工程级最小可行闭环”。它不炫技但把「用户→角色→权限→菜单/按钮」这条链路压进 200 行核心 PHP 逻辑 3 个 Layui 模板 4 张 MySQL 表里部署即用。Layui 的模块化加载机制让权限菜单渲染快到几乎无感TP6 的中间件和多态查询让角色权限校验逻辑干净得像白纸更重要的是——它不依赖 Node.js 构建、不卡 CDN、不因浏览器版本翻车。如果你需要的是“今天写完明天上线后天教行政同事自己改菜单”这套组合不是退而求其次而是经过血泪验证的最优解。2. 从零搭建四张表定乾坤TP6 中间件接管权限校验2.1 四张核心数据表设计不加冗余字段只留 RBAC 必需字段RBAC 模型本质就四要素用户user、角色role、权限permission、关联关系role_access。我们严格按最小集设计避免后期字段膨胀导致 SQL 复杂度飙升表名字段说明关键约束为什么这样设tp_userid,username,password,status,role_idrole_id非空外键TP6 支持单角色快速查多角色用中间表会拖慢登录速度先做单角色 MVP后续扩展用user_role表tp_roleid,name,status,remarkname唯一索引角色名直接用于权限判断如if($role-name admin)比查 ID 更直觉tp_menuid,title,path,icon,sort,pid,status,auth_ruleauth_rule存控制器/方法标识如admin.User/indexLayui 菜单渲染靠path和pid权限校验靠auth_rule二者分离避免耦合tp_auth_ruleid,name,title,type,statusname唯一如admin.User/delete按 TP6 官方规范name是权限唯一标识type1表示菜单type2表示按钮操作提示tp_menu.auth_rule和tp_auth_rule.name必须严格一致才能匹配。例如菜单“用户删除”对应auth_ruleadmin.User/delete其权限规则表里必须有nameadmin.User/delete且status1。这是整个权限链路的锚点错一个字符就失效。建表 SQLMySQL 5.7-- 用户表简化版密码已加密 CREATE TABLE tp_user ( id int(11) unsigned NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL DEFAULT , password varchar(100) NOT NULL DEFAULT , status tinyint(1) NOT NULL DEFAULT 1, role_id int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY role_id (role_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 角色表 CREATE TABLE tp_role ( id int(11) unsigned NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL DEFAULT , status tinyint(1) NOT NULL DEFAULT 1, remark varchar(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 菜单表含权限标识 CREATE TABLE tp_menu ( id int(11) unsigned NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL DEFAULT , path varchar(100) NOT NULL DEFAULT , icon varchar(50) DEFAULT NULL, sort int(11) NOT NULL DEFAULT 0, pid int(11) NOT NULL DEFAULT 0, status tinyint(1) NOT NULL DEFAULT 1, auth_rule varchar(100) DEFAULT NULL, PRIMARY KEY (id), KEY pid (pid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 权限规则表按钮级权限在此定义 CREATE TABLE tp_auth_rule ( id int(11) unsigned NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL DEFAULT , title varchar(100) NOT NULL DEFAULT , type tinyint(1) NOT NULL DEFAULT 1 COMMENT 1:菜单,2:操作, status tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 TP6 权限中间件三步拦截拒绝未授权访问TP6 的中间件是权限校验的天然载体。我们不写全局钩子而是精准注入到app/middleware.php的route层确保每个请求路径进来时立刻判断提取当前请求的控制器/方法标识如admin.User/index查用户角色 → 查该角色拥有的所有auth_rule.name比对请求标识是否在权限列表中创建中间件app/middleware/AuthCheck.php?php declare (strict_types 1); namespace app\middleware; use think\Request; use think\Response; use think\facade\Db; class AuthCheck { public function handle(Request $request, \Closure $next) { // 1. 获取当前登录用户假设已通过 login 中间件存 session $user session(user); if (!$user || !isset($user[id])) { return redirect(/login)-code(302); } // 2. 获取当前请求的 auth_rule 标识格式控制器.方法 $controller $request-controller(); $action $request-action(); $ruleName strtolower($controller . . . $action); // 统一小写避免大小写问题 // 3. 查询该用户角色拥有的所有权限规则 $roleId (int)$user[role_id]; if ($roleId 0) { return json([code 403, msg 无角色权限])-code(403); } // 关键用一条 JOIN 查出角色所有启用的权限规则 $rules Db::table(tp_role) -alias(r) -join(tp_auth_rule, r.id tp_auth_rule.role_id, LEFT) // 注意此处需先建 role_auth_rule 关联表见 2.3 -where(r.id, $roleId) -where(tp_auth_rule.status, 1) -column(tp_auth_rule.name); // 若未查到规则或规则中不含当前请求标识则拒绝 if (empty($rules) || !in_array($ruleName, $rules)) { // 特殊放行首页、个人中心等公共页面 $publicRules [index.index, login.index, login.logout]; if (!in_array($ruleName, $publicRules)) { return json([code 403, msg 无访问权限])-code(403); } } return $next($request); } }参数说明$ruleName strtolower($controller . . . $action)强制小写解决 Windows/Linux 路径大小写差异Db::table(tp_role)-join(...)此处实际应关联role_auth_rule中间表见 2.3示例为简化逻辑真实代码需修正$publicRules白名单机制避免登录页被拦截这是生产环境必加的安全兜底。2.3 角色-权限关联表用中间表支持多角色、多权限灵活绑定TP6 默认不提供角色权限多对多关系必须手动建中间表tp_role_auth_ruleCREATE TABLE tp_role_auth_rule ( id int(11) unsigned NOT NULL AUTO_INCREMENT, role_id int(11) NOT NULL DEFAULT 0, auth_rule_id int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY role_rule (role_id,auth_rule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;然后修改中间件中的查询逻辑替换 2.2 中的 JOIN// 正确写法通过中间表关联查询 $rules Db::table(tp_role_auth_rule) -alias(rar) -join(tp_auth_rule, rar.auth_rule_id tp_auth_rule.id) -where(rar.role_id, $roleId) -where(tp_auth_rule.status, 1) -column(tp_auth_rule.name);为什么不用role_id直接存tp_auth_rule表因为一个角色可绑多个权限一个权限可被多个角色使用。硬编码role_id字段会导致tp_auth_rule表膨胀且无法复用。中间表是关系型数据库处理多对多的标准解法TP6 的hasManyThrough关系也依赖它。3. Layui 侧菜单动态渲染与按钮权限隐藏不写一行 JS 就生效3.1 后端吐出菜单 JSON按 pid 递归生成 Layui 所需的 tree 结构Layui 的nav导航菜单要求数据是嵌套数组形如[ {title:用户管理,icon:fa-user,href:/admin/user/index,spread:false,children:[ {title:用户列表,href:/admin/user/index}, {title:添加用户,href:/admin/user/add} ]} ]我们在app/controller/Admin/Index.php中写一个getMenu()方法不依赖前端 JS 拼接纯 PHP 递归生成public function getMenu() { $userId session(user.id); if (!$userId) { return json([code 401, msg 未登录]); } // 1. 获取用户角色ID $roleId Db::name(user)-where(id, $userId)-value(role_id); if (!$roleId) { return json([code 403, msg 无角色]); } // 2. 获取该角色所有启用的菜单权限type1 $menuIds Db::table(tp_role_auth_rule) -alias(rar) -join(tp_auth_rule, rar.auth_rule_id tp_auth_rule.id) -where(rar.role_id, $roleId) -where(tp_auth_rule.type, 1) // 只取菜单 -where(tp_auth_rule.status, 1) -column(tp_auth_rule.name); // 得到 [admin.User/index, admin.User/add] // 3. 根据权限标识查菜单表注意auth_rule 字段存的就是 controller.action $menuList Db::name(menu) -where(status, 1) -where(auth_rule, in, $menuIds) -order(sort, asc) -select() -toArray(); // 4. 递归构建树结构关键用 auth_rule 匹配不是 id $tree $this-buildMenuTree($menuList, 0); return json([code 0, data $tree]); } private function buildMenuTree($list, $pid) { $tree []; foreach ($list as $item) { if ($item[pid] $pid) { $children $this-buildMenuTree($list, $item[id]); if (!empty($children)) { $item[children] $children; } // Layui 要求 href 是相对路径去掉 admin/ 前缀 $item[href] str_replace(admin., /, $item[auth_rule]); $tree[] $item; } } return $tree; }逻辑说明第 2 步查出的是权限规则名如admin.User/index第 3 步用它去tp_menu.auth_rule字段精确匹配确保菜单只显示用户有权限的项str_replace(admin., /, $item[auth_rule])将admin.User/index转为/user/index适配 Layui 的href跳转buildMenuTree()递归时用pid判断层级而非auth_rule因为菜单结构由pid决定权限由auth_rule控制二者解耦。3.2 Layui 模板中动态加载菜单用layui.use([element, layer])一行搞定在view/layout/layout.html底部加入script src/static/layui/layui.js/script script layui.use([element, layer, jquery], function(){ var element layui.element; var $ layui.jquery; var layer layui.layer; // 1. 请求菜单数据 $.get(/admin/index/getMenu, function(res){ if(res.code ! 0){ layer.msg(res.msg || 菜单加载失败, {icon:5}); return; } // 2. 渲染左侧导航Layui nav var menuHtml ; res.data.forEach(function(item){ if(item.children item.children.length 0){ menuHtml li classlayui-nav-item; menuHtml a hrefjavascript:; item.title /a; menuHtml dl classlayui-nav-child; item.children.forEach(function(child){ menuHtml dda lay-href child.href child.title /a/dd; }); menuHtml /dl/li; } else { menuHtml li classlayui-nav-itema lay-href item.href item.title /a/li; } }); $(#LAY-system-side).html(menuHtml); element.render(nav); }); }); /script参数说明lay-href是 Layui 的路由跳转属性配合router模块可实现无刷新切换element.render(nav)是必须调用的重载方法否则新插入的 DOM 不会被识别为导航整个过程不依赖任何第三方 JS 插件纯 Layui 原生能力。3.3 按钮级权限控制用>!-- 按钮模板 -- button typebutton classlayui-btn layui-btn-sm>{__DEFINE__} ?php $userId session(user.id); $roleId Db::name(user)-where(id, $userId)-value(role_id); $authRules Db::table(tp_role_auth_rule) -alias(rar) -join(tp_auth_rule, rar.auth_rule_id tp_auth_rule.id) -where(rar.role_id, $roleId) -where(tp_auth_rule.type, 2) // 按钮权限 type2 -where(tp_auth_rule.status, 1) -column(tp_auth_rule.name); ? script window.authRules ? json_encode($authRules) ?; /script为什么这样更安全按钮 HTML 本身由后端决定是否输出JS 只是二次隐藏防误点window.authRules在页面渲染时已确定无法被前端篡改除非 XSS那是另一层防护>// URL 加 ?t? time() ?或后端设置响应头 header(Cache-Control: no-cache); header(Pragma: no-cache);同时TP6 的Db::cache(true)对column()查询无效必须手动清除\think\Cache::tag(menu)-clear(); // 提前给菜单查询打标签4.3 现象管理员角色能访问所有菜单但按钮权限不生效原因tp_auth_rule.type字段未区分菜单type1和按钮type2导致按钮权限查询时误查菜单规则。解决建表时明确type含义并在所有查询中强制加where(type, 2)用tp_auth_rule.title字段存按钮中文名如“删除用户”避免name字段语义混乱。4.4 现象IE11 下 Layui 菜单折叠图标不显示点击无反应原因Layui 2.5.6 的icon字段依赖 Font Awesome但 IE11 不支持font-feature-settings导致图标字体渲染失败。解决放弃 FA改用 Layui 内置图标类名layui-icon-xxx或在tp_menu.icon字段存类名而非 Unicode!-- 改为 -- i classlayui-icon layui-icon-add/i !-- 而非 -- i classfa fa-plus/i4.5 现象TP6 开启app_debugfalse后权限中间件报错 500原因中间件中session(user)在关闭 debug 时返回 null因 session 配置未生效。解决检查config/session.php中type是否为file默认并确认runtime/session/目录可写更稳妥做法是在中间件开头加 session 初始化if (!session(?user)) { return redirect(/login)-code(302); }5. 进阶技巧用 TP6 的命令行快速初始化权限、Layui 表单一键导出 Excel5.1 用 TP6 命令行批量初始化菜单与权限告别手动 INSERT每次新项目都要手写几十条INSERT INTO tp_menu太原始。TP6 的think命令行可封装成初始化脚本创建app/command/InitAuth.php?php declare (strict_types 1); namespace app\command; use think\console\Command; use think\console\Input; use think\console\Output; use think\facade\Db; class InitAuth extends Command { protected function configure() { $this-setName(auth:init) -setDescription(初始化权限菜单和规则); } protected function execute(Input $input, Output $output) { // 1. 清空旧数据生产环境慎用加 confirm Db::name(menu)-delete(); Db::name(auth_rule)-delete(); Db::name(role_auth_rule)-delete(); // 2. 插入基础菜单按 pid 层级写死 $menus [ [title首页,path/index,iconlayui-icon-home,sort1,pid0,auth_ruleindex.index], [title用户管理,path/user,iconlayui-icon-user,sort2,pid0,auth_ruleadmin.User/index], [title用户列表,path/user/index,icon,sort1,pid2,auth_ruleadmin.User/index], [title添加用户,path/user/add,icon,sort2,pid2,auth_ruleadmin.User/add], ]; Db::name(menu)-insertAll($menus); // 3. 插入权限规则type1 菜单type2 操作 $rules [ [nameindex.index,title首页,type1,status1], [nameadmin.User/index,title用户列表,type1,status1], [nameadmin.User/add,title添加用户,type2,status1], ]; Db::name(auth_rule)-insertAll($rules); // 4. 绑定管理员角色role_id1拥有全部权限 $ruleIds Db::name(auth_rule)-column(id); $bindData array_map(function($id) { return [role_id1, auth_rule_id$id]; }, $ruleIds); Db::name(role_auth_rule)-insertAll($bindData); $output-writeln(✅ 权限初始化完成); } }执行命令php think auth:init价值点新项目git clone后composer installphp think auth:init两步到位所有菜单/权限数据集中管理避免分散在 SQL 文件中难以维护role_auth_rule绑定逻辑可扩展为按角色配置文件如config/auth/admin.php实现配置驱动。5.2 Layui 表单提交后用 TP6 的Excel扩展一键导出带权限过滤的数据用户点击“导出 Excel”按钮后端需校验该用户是否有admin.User/export权限再执行导出先加权限规则INSERT INTO tp_auth_rule (name, title, type, status) VALUES (admin.User/export, 导出用户, 2, 1);在控制器中加导出方法public function export() { // 权限校验复用中间件逻辑 $user session(user); $roleId (int)$user[role_id]; $hasExport Db::table(tp_role_auth_rule) -alias(rar) -join(tp_auth_rule, rar.auth_rule_id tp_auth_rule.id) -where(rar.role_id, $roleId) -where(tp_auth_rule.name, admin.User/export) -where(tp_auth_rule.status, 1) -find(); if (!$hasExport) { return json([code403, msg无导出权限]); } // 查询数据自动带权限过滤普通用户只能看自己的数据 $where []; if ($user[role_id] ! 1) { // 非管理员 $where[id] $user[id]; // 示例只导出本人数据 } $list Db::name(user)-where($where)-select(); // 使用 thinkphp-excel 扩展composer require phpoffice/phpspreadsheet $spreadsheet new \PhpOffice\PhpSpreadsheet\Spreadsheet(); $sheet $spreadsheet-getActiveSheet(); $sheet-setCellValue(A1, ID)-setCellValue(B1, 用户名)-setCellValue(C1, 状态); $row 2; foreach ($list as $item) { $sheet-setCellValue(A.$row, $item[id]) -setCellValue(B.$row, $item[username]) -setCellValue(C.$row, $item[status] ? 启用 : 禁用); $row; } $writer new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet); header(Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); header(Content-Disposition: attachment;filenameuser_export_.date(YmdHis)..xlsx); $writer-save(php://output); exit; }前端按钮加>button typebutton classlayui-btn layui-btn-sm style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
返回列表