资讯动态

layui 2.5.6 与 ThinkPHP6 打造 RBAC 权限管理后台完整实战

发布时间:2026/10/6 3:44:19 来源:尧图企业网站定制
简介layui2.5.6 与 ThinkPHP6.0.2 整合的权限管理后台面向 PHP 初中级开发者与需要快速搭建后台管理系统的团队定位清晰、开箱即用。这套后台围绕 RBAC 权限控制模型设计覆盖用户角色分配、节点权限管理、登录认证等典型场景代码分层清晰适合作为课程设计、毕业设计或企业内部系统的基础工程参考。压缩包约 6.1MB共 1098 个文件包含 481 个 PHP 后端逻辑、139 个 JS 与 30 个 CSS 前端资源、36 个 HTML 页面模板另附 120 个 GIF 演示动图、94 张 PNG 图标素材及 SQL、env、composer 等辅助配置结构完整、便于按模块检索。项目遵循 Composer 依赖管理与 PSR 标准集成 ThinkPHP6 服务容器、事件监听与多应用支持前端采用 layui 的栅格、表格、弹层与表单组件前后端职责划分明确并配套持续集成配置与数据库初始化脚本调整 .env 的数据库连接即可快速还原运行。已有 2283 人学习下载无论是想理解权限控制实现思路还是掌握 layui 后台界面的搭建方式都能从中获得可直接复用的代码与排错经验。1. 这套 layui 2.5.6 tp6.0.2 权限管理后台是给谁准备的后台管理系统翻来覆去就那几样登录、菜单、列表、权限。我刚交付的项目正好是这套组合前端用 layui 2.5.6后端用 ThinkPHP 6.0.2大家习惯叫 tp6需求是把权限管理后台做成“用户分角色、角色挂菜单和按钮权限、接口不能被越权调用”。这套方案的定位很明确不引微前端不上重量级全家桶用最朴素的方式把基于角色的访问控制模型跑起来特别适合快速交付的中小后台、内部运营系统以及需要接手老项目继续往里加功能的 PHP 工程师。它能解决三个具体问题第一每个用户登录后看到的菜单不一样菜单节点由数据库控制而不是写死在前端模板里第二接口层有统一拦截就算前端漏隐藏了某个按钮后端一样拒绝请求第三新增功能时只要在权限表里加一条记录再分配给对应角色就行不用为每个角色单独改代码。对新手来说这套东西看得见摸得着对熟手来说省得重复造轮子。我实际搭的时候按这个顺序推进先把 RBAC 表结构定下来再写登录和权限中间件接着接前端菜单与表格最后配一套自查脚本。整个过程不依赖第三方 admin 框架每一步都能看到数据是怎么流转的。2. 先立住 RBAC 模型五张表怎么设计判断逻辑怎么抽出来2.1 为什么选 RBAC 而不是写死在控制器里权限管理后台最容易翻车的写法是在控制器里到处塞判断条件if (session(admin_type) ! admin) { return json([code 0, msg 没有权限]); }这种写法前期跑得欢角色一多就变成灾难产品说编辑也能删用户你去翻控制器改条件运营说某个操作只给主管看你又得去改一段 if。更麻烦的是你没法回答“哪些角色能看哪些功能”这个问题只能逐个控制器翻代码。RBAC基于角色的访问控制的做法是加一个中间层用户不直接拥有权限而是通过角色关联权限。我一般用五张表落地admin_user 存用户admin_role 存角色admin_permission 存每一个可校验的权限节点admin_user_role 和 admin_role_permission 两张关联表分别做用户-角色、角色-权限的多对多绑定。这样一来权限的授予和回收都变成数据库里的增删操作不用动代码。对比项写死在控制器RBAC新增角色改代码重新上线配一条记录回收权限翻代码找条件删关联记录审计追踪很难查表就能答学习成本低中等但一次到位2.2 建表 SQL用户、角色、权限和两张关联表下面这套表结构是权限管理后台里最常见的设计我直接给出可执行的建表语句CREATE TABLE admin_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL DEFAULT , password varchar(255) NOT NULL DEFAULT , nickname varchar(50) NOT NULL DEFAULT , status tinyint(1) NOT NULL DEFAULT 1, last_login_time int(11) DEFAULT NULL, create_time int(11) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin_role ( id int(11) NOT NULL AUTO_INCREMENT, role_name varchar(50) NOT NULL DEFAULT , remark varchar(255) NOT NULL DEFAULT , is_super tinyint(1) NOT NULL DEFAULT 0 COMMENT 是否超级管理员, status tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin_permission ( id int(11) NOT NULL AUTO_INCREMENT, pid int(11) NOT NULL DEFAULT 0 COMMENT 父节点 id顶级为 0, title varchar(50) NOT NULL DEFAULT COMMENT 菜单/按钮名称, name varchar(100) NOT NULL DEFAULT COMMENT 权限标识如 user/list, icon varchar(50) NOT NULL DEFAULT , sort int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin_user_role ( user_id int(11) NOT NULL, role_id int(11) NOT NULL, PRIMARY KEY (user_id, role_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin_role_permission ( role_id int(11) NOT NULL, permission_id int(11) NOT NULL, PRIMARY KEY (role_id, permission_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个设计点说一下。第一关联表用联合主键天然防止同一条记录被重复绑定。第二admin_permission 的 name 字段设唯一索引同一权限标识不允许出现两次避免权限判断时产生歧义。第三admin_permission 的 pid 字段有两个作用顶层菜单用 pid0子菜单和按钮节点用 pid 指向父节点这样一张表既能生成菜单树又能承载按钮级权限标识。第四字符集统一用 utf8mb4不然中文菜单名容易乱码这是老生常谈但确实常踩。配套的初始数据可以这样给INSERT INTO admin_role (id, role_name, is_super) VALUES (1, 超级管理员, 1); INSERT INTO admin_permission (id, pid, title, name) VALUES (1, 0, 用户管理, ), (2, 1, 用户列表, user/list), (3, 1, 新增用户, user/add), (4, 1, 编辑用户, user/edit);2.3 把判断逻辑抽成一个方法传入权限标识返回是否通过建好表之后先把“当前用户是否有某个权限”这个判断抽出来。我习惯放在项目自带的公共基类app\BaseController的派生类里子控制器直接继承就能用。?php // app/controller/AdminBase.php namespace app\controller; use app\BaseController; use think\facade\Cache; use think\facade\Session; class AdminBase extends BaseController { /** * 校验当前用户是否拥有指定权限标识 * param string $rule 例如 user/edit */ protected function checkRule(string $rule): bool { // 超级管理员直接放行 if (Session::get(admin_is_super)) { return true; } $userId Session::get(admin_user_id); if (!$userId) { return false; } // 优先读缓存缓存里没有就临时查库 $perms Cache::get(admin_perms_ . $userId); if (!$perms) { $roleIds \app\model\AdminUserRole::where(user_id, $userId)-column(role_id); $permIds \app\model\AdminRolePermission::whereIn(role_id, $roleIds)-column(permission_id); $perms \app\model\AdminPermission::whereIn(id, $permIds)-column(name); Cache::set(admin_perms_ . $userId, $perms, 3600); } return is_array($perms) in_array($rule, $perms); } }这里把权限判断收敛到一个方法里好处是后面不管用中间件统一拦截还是在某个接口内部做局部校验逻辑都是同一份。判断规则用”控制器/方法”的字符串比如 user/edit好处是直观看到标识就知道它对应哪个功能新人维护权限表时不需要查代码。超级管理员用 is_super 字段而不是硬编码 role_id1因为角色表换个环境以后主键不一定还是 1。3. TP6 登录认证与权限中间件session 与缓存怎么配合3.1 登录接口验证密码、写 session、生成权限缓存第 2 章把表结构和判定逻辑立住了接下来要让用户在 tp6 里真正登进去。登录接口的职责不止是验证账号密码还要把权限数据准备好。?php // app/controller/Login.php namespace app\controller; use app\model\AdminUser; use app\model\AdminUserRole; use app\model\AdminRole; use app\model\AdminRolePermission; use app\model\AdminPermission; use think\facade\Cache; use think\facade\Session; class Login extends AdminBase { public function login() { if ($this-request-isPost()) { $username $this-request-param(username); $password $this-request-param(password); // 用 password_verify 校验 password_hash 生成的散列 $user AdminUser::where(username, $username)-find(); if (!$user || !password_verify($password, $user-password)) { return json([code 0, msg 用户名或密码错误]); } // 查出用户关联的角色 id $roleIds AdminUserRole::where(user_id, $user-id)-column(role_id); // 判断是否超级管理员 $isSuper AdminRole::whereIn(id, $roleIds)-where(is_super, 1)-count() 0; // 组装权限标识数组超级管理员拿全部普通角色按关联表查 if ($isSuper) { $perms AdminPermission::column(name); } else { $permIds AdminRolePermission::whereIn(role_id, $roleIds)-column(permission_id); $perms AdminPermission::whereIn(id, $permIds)-column(name); } // 会话数据 Session::set(admin_user_id, $user-id); Session::set(admin_username, $user-username); Session::set(admin_is_super, $isSuper); Session::set(admin_role_ids, $roleIds); // 权限标识放进缓存key 里带用户 id保证彼此隔离 Cache::set(admin_perms_ . $user-id, $perms, 3600); return json([code 1, msg 登录成功]); } return view(login); } }这段代码有三个参数要说清楚。第一密码校验用的是password_hash/password_verify这套原生函数如果你之前还在用 md5 加盐建议趁这个机会换掉。第二权限标识数组admin_perms_user_id写进缓存而不是 session是为了后续角色权限变动时可以按用户 id 精确失效不用等 session 自然过期。第三如果这里view(login)报错先确认 composer 里有没有topthink/think-view扩展tp6 的视图功能不是默认自带的。3.2 用中间件拦截请求未登录跳转无权限返回登录做完接下来是权限管理后台最关键的一道闸中间件。tp6 的中间件是在请求进入控制器之前执行的正好用来做登录态和权限的双重校验。?php // app/middleware/AuthCheck.php namespace app\middleware; use app\model\AdminPermission; use app\model\AdminRolePermission; use app\model\AdminUserRole; use think\facade\Cache; use think\facade\Session; class AuthCheck { // 不需要校验登录态的控制器 protected $allow [login, captcha]; public function handle($request, \Closure $next) { // parse_name 把驼峰控制器名转成下划线小写 $controller strtolower(parse_name($request-controller())); // 白名单直接放行 if (in_array($controller, $this-allow)) { return $next($request); } // 未登录统一跳登录页 $userId Session::get(admin_user_id); if (!$userId) { return redirect(/login); } // 超级管理员放行 if (Session::get(admin_is_super)) { return $next($request); } // 拼权限标识user/edit 这种格式 $rule $controller . / . strtolower($request-action()); // 读权限缓存没有则重新查询 $cacheKey admin_perms_ . $userId; $perms Cache::get($cacheKey); if (!$perms) { $roleIds AdminUserRole::where(user_id, $userId)-column(role_id); $permIds AdminRolePermission::whereIn(role_id, $roleIds)-column(permission_id); $perms AdminPermission::whereIn(id, $permIds)-column(name); Cache::set($cacheKey, $perms, 3600); } if (!in_array($rule, (array) $perms)) { return json([code 0, msg 没有操作权限]); } return $next($request); } }中间件注册有两种常见做法。项目里如果只有后台模块直接在app/middleware.php里注册为全局中间件最省事// app/middleware.php return [ \app\middleware\AuthCheck::class, ];如果控制器里还混着不要求登录的开放接口就把中间件挂到路由分组上// route/app.php use think\facade\Route; Route::group(, function () { Route::get(user/list, user/list); Route::post(user/edit, user/edit); // 其他后台路由 })-middleware(\app\middleware\AuthCheck::class);我一般优先用全局注册加白名单的方式因为后台项目大部分接口都需要登录态白名单里通常只有 login 和 captcha 两个控制器。中间件里用parse_name做控制器名转换是因为 tp6 的路由规则是下划线风格而权限表里的标识也统一存下划线格式两边才能对得上。3.3 权限缓存刷新角色变动后不能等它自己过期权限缓存设置 3600 秒过期本意是减少数据库查询但会引入一个经典问题管理员给某个角色加了新权限这个角色的用户要等一小时甚至更久才能生效。我的做法是在角色保存权限的同时主动清掉受影响的缓存。// app/controller/Role.php 中保存角色权限的方法 public function setPermission() { $roleId $this-request-param(role_id); $permIds $this-request-param(perm_ids/a, []); // 先删旧关联再批量写入 Db::name(admin_role_permission)-where(role_id, $roleId)-delete(); $rows []; foreach ($permIds as $permId) { $rows[] [role_id $roleId, permission_id $permId]; } Db::name(admin_role_permission)-insertAll($rows); // 找出拥有该角色的所有用户逐个清掉权限缓存 $userIds AdminUserRole::where(role_id, $roleId)-column(user_id); foreach ($userIds as $uid) { Cache::delete(admin_perms_ . $uid); } return json([code 1, msg 权限已更新]); }这个设计的核心原则是权限数据的变更要让它在下一个请求就生效不能依赖缓存过期。如果你不想为每个变更操作写清理逻辑还有一个折中办法把缓存时间缩短到 60 秒或者干脆不缓存但那样性能会差一些。中小后台我建议保留缓存然后把上面这段清理逻辑固化在角色权限的保存方法里后面就不会出现“明明给了权限用户却说没有”的翻车现场。4. layui 2.5.6 前端接入动态菜单、表格渲染与按钮显隐4.1 用 iframe 布局把菜单和内容区解耦layui 2.5.6 时代的后台布局最常见的还是左侧 nav 菜单加右侧 iframe。为什么不直接做单页切换因为后台页面一旦变复杂列表的查询条件、分页状态、编辑弹层互相干扰iframe 每个页面独立运行反而更好维护。另外 2.5.6 这个版本的 admin 组件还没有后来那样完善用 iframe 是当时项目里最不容易翻车的方案。页面骨架如下菜单区域只有一个空容器内容由后续脚本填充div classlayui-layout layui-layout-admin div classlayui-header div classlayui-logo权限管理后台/div /div div classlayui-side div classlayui-side-scroll ul classlayui-nav layui-nav-tree idside-menu lay-filterside-menu/ul /div /div div classlayui-body iframe idcontent-frame src/user/list frameborder0 width100% height100%/iframe /div /div4.2 登录后拉取菜单用 layui nav 渲染侧边栏菜单接口返回的是当前用户可见的树同时带上权限标识数组前端按钮显隐也需要它。layui.use([jquery, element], function () { var $ layui.jquery; var element layui.element; $.get(/index/menu, function (res) { var html ; res.data.forEach(function (menu) { if (menu.children menu.children.length 0) { html li classlayui-nav-item; html a hrefjavascript:; menu.title /a; html dl; menu.children.forEach(function (child) { html dda hrefjavascript:;>layui.use([table], function () { var table layui.table; table.render({ elem: #user-table, url: /user/list, page: true, cols: [[ { field: id, title: ID, width: 80 }, { field: username, title: 用户名 }, { field: nickname, title: 昵称 }, { field: status, title: 状态, templet: function (d) { return d.status 1 ? 启用 : 禁用; }} ]] }); });tp6 侧的列表接口要严格返回这个结构// app/controller/User.php public function list() { $page $this-request-param(page, 1); $limit $this-request-param(limit, 10); $total Db::name(admin_user)-count(); $list Db::name(admin_user) -page($page, $limit) -select() -toArray(); return json([ code 0, msg , count $total, data $list ]); }这里解释一下联动逻辑layui 2.5.6 请求时会自动带上page和limit两个参数刚好对应 tp6Db::page($page, $limit)的两个参数所以不用做额外映射。返回结构里code必须等于 0table 才认为是成功。data不能直接丢 TP6 的 Collection 对象要先toArray()否则 table 拿不到数据。表格空白时第一件事是打开浏览器调试面板看响应 JSON而不是先怀疑表格组件。4.4 操作按钮按权限渲染laytpl 模板里判断 authPerms菜单能动态生成了表格能出数据了最后是操作列的按钮控制。常见做法是在视图层把权限数组注入全局配合 layui 的 laytpl 模板语法做条件渲染。!-- 视图层底部注入当前用户的权限标识 -- script window.authPerms ?php echo json_encode(session(admin_perms)); ?; /scriptscript typetext/html iduser-toolbar {{# if(window.authPerms.indexOf(user/edit) -1){ }} a classlayui-btn layui-btn-xs lay-eventedit编辑/a {{# } }} {{# if(window.authPerms.indexOf(user/delete) -1){ }} a classlayui-btn layui-btn-danger layui-btn-xs lay-eventdelete删除/a {{# } }} /scriptlaytpl 的{{# ... }}里可以执行任意 JavaScript 语句所以直接判断window.authPerms是否存在对应的权限标识。注意如果页面是用 iframe 打开的window.authPerms是子窗口自己的 window只要在子页面模板里注入过这里就能拿到。还要强调一遍前端按钮隐藏只是改善界面体验它不是安全机制接口必须走第 3 章的中间件校验不然用户在地址栏直接输入接口地址就绕过了。5. 权限管理后台常见问题排查清单五个翻车点的现象、原因与解决5.1 表格翻页就空白layui 的 page 参数和 TP6 分页没对齐现象列表第一页数据正常点击下一页或者切换每页条数后表格区域直接空白。 原因layui 2.5.6 发送的请求参数固定叫page和limit但有的后端代码习惯性用了 TP5 时代的page加list_rows或者干脆写死只查第一页。 解决列表方法里统一用$page $this-request-param(page, 1); $limit $this-request-param(limit, 10);再把返回结构固定成code、msg、count、data四个键data必须是数组。这样无论前端怎么翻页后端都能正确响应。5.2 session 直接报错topthink/think-session 扩展没装现象调用Session::get()或session()辅助函数时抛出类似Class think\facade\Session not found的异常。 原因tp6 的 session 功能不像 tp5 那样内置它由独立的topthink/think-session包提供。从 tp5 迁移过来的老代码最容易踩这个坑。 解决进入项目目录执行composer require topthink/think-session装完以后门面和辅助函数就能正常使用。如果还报错检查runtime/session目录可写权限Windows 环境偶尔会有这个目录权限缺失的问题。5.3 管理员被中间件拦截超级管理员角色没放行现象管理员账号登录成功但打开任何功能页都提示“没有操作权限”或者被反复重定向到登录页。 原因中间件里只判断了普通权限标识没有给超级管理员单独开绿灯或者管理员角色压根没有绑定任何权限节点。 解决在 admin_role 表加is_super字段登录时查询该用户关联的角色中是否存在is_super1把结果写入session(admin_is_super)。中间件第一步就判断这个标记命中直接return $next($request)。不要硬编码role_id1环境一变主键就不是 1 了。5.4 按钮权限绕过前端只隐藏不算数接口必须校验现象前端按 4.4 隐藏了删除按钮但用户直接在浏览器地址栏输入/user/delete数据还是被删掉了。 原因前端隐藏按钮只影响界面展示请求本身没有经过任何权限拦截。这类问题严格说不是 bug而是方案设计遗漏。 解决权限校验必须落在中间件层也就是请求进入控制器之前。第 3 章的 AuthCheck 做的事就是这个每个非白名单请求拼出权限标识再和用户权限集比对。前端按钮显隐只能算体验优化后端接口校验才是权限管理后台的安全底线。5.5 菜单层级乱了权限表 pid 和排序列混用现象新增一个子菜单后它跑到顶级菜单栏里或者挂在完全错误的父节点下面。 原因菜单接口没有按pid做递归组装直接把admin_permission表全量平铺返回了还有一种情况是有人把sort字段当成pid用两层菜单时看着正常三层就开始乱。 解决菜单接口里先按pid分组再递归构建树只有pid0的数据才是顶级菜单。前端渲染时根据children判断是父菜单还是叶子菜单。sort只做同级排序不承担层级关系逻辑上要和pid分开。6. 权限节点自查脚本把控制器方法跟权限表逐行比对权限管理后台上线一段时间后最容易出现的问题是控制器里新加了一个方法但权限表里忘了配节点导致功能没人能用反过来权限表里配了节点但控制器方法已经删了留下一条永远用不到的脏数据。人工排查很费劲我写了一个 tp6 命令行脚本来自动比对。?php // app/command/CheckPermission.php namespace app\command; use think\console\Command; use think\console\Input; use think\console\Output; use think\facade\Db; class CheckPermission extends Command { protected function configure() { $this-setName(check:permission); $this-setDescription(核对控制器方法与权限节点表); } protected function execute(Input $input, Output $output) { // 扫描 app/controller 下的控制器文件 $files glob(app()-getBasePath() . controller/*.php); if (!$files) { $output-writeln(没有找到控制器文件); return; } // 读取权限表里已有的权限标识 $rules Db::name(admin_permission)-column(name); // 不需要检查的控制器 $skipControllers [adminbase, login]; foreach ($files as $file) { $controller basename($file, .php); if (in_array(strtolower($controller), $skipControllers)) { continue; } // 用正则提取 public function 方法名 $content file_get_contents($file); preg_match_all(/public\sfunction\s(\w)/, $content, $m); foreach ($m[1] as $action) { // 跳过下划线开头的方法比如 _empty if (str_starts_with($action, _)) { continue; } $rule strtolower($controller . / . $action); if (!in_array($rule, $rules)) { $output-writeln(缺少权限节点: . $rule); } } } $output-writeln(检查完成); } }脚本思路不复杂遍历控制器文件正则提取所有public function方法名拼成权限标识再和admin_permission.name比对。要注意parse_name的用法脚本里我直接strtolower($controller)如果你的控制器名是驼峰比如UserGroup权限表里存的是user_group/list这里就要改成strtolower(parse_name($controller))和中间件保持一致。注册命令的方式是在config/console.php里加上// config/console.php return [ commands [ \app\command\CheckPermission::class, ], ];然后命令行执行php think check:permission输出结果会列出所有缺失节点对照着去后台权限管理里补充即可。我实际用的时候还会顺手把多余节点也扫出来把角色权限关联表里permission_id对应的name拿来做反向比对就能发现哪些节点已经没控制器方法在用可以清理。这步是权限管理后台上线后最实用的日常维护技巧能避免节点表越滚越脏。我一开始也没写这个脚本结果吃了好几次亏有次新接口上线忘了配权限用户在浏览器里看着菜单却没权限有次删了控制器方法但权限节点还在审计时解释不清为什么有这个入口。后来把它固化成命令每次新增功能提交前跑一遍问题就挡在了上线之前。权限这块没有玄学靠的就是一遍遍核对。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑