资讯动态

layui2.5.6+ThinkPHP6.0.2权限管理后台实战:从登录鉴权到节点拦截

发布时间:2026/10/18 9:01:31 来源:尧图企业网站定制
简介这是一套基于 layui2.5.6 前端组件库与 ThinkPHP6.0.2 后端框架搭建的权限管理后台源码面向需要快速构建企业级管理系统的 PHP 开发者也适合初学者理解前后端协同与 RBAC 权限控制的实现思路。压缩包共 1098 个文件约 6.1MB以 481 个 php 业务与框架代码、139 个 js 脚本、36 个 html 模板、30 个 css 样式为主另含图片、字体、json 配置及 sql 建表脚本等资源目录结构清晰涵盖 public 入口、think 核心、config 配置与依赖管理文件。项目内置角色分配与权限校验模型配合 .env 环境配置可适配不同部署环境并遵循 Composer 依赖管理与持续集成等工程实践。目前已有 2283 人学习下载读者可据此掌握后台权限模块的搭建流程、目录组织方式与二次开发要点快速落地自己的管理后台。1. 后台权限系统还能这么搭layui2.5.6 配 tp6.0.2 的落地体验很多开发者一提到权限管理后台第一反应就是上 Vue Element 或者 React Ant Design前后端分离、JWT、动态路由一套组合拳打下来光环境搭建就得折腾半天。但如果你的需求只是给内部运营团队做一个稳定的管理端服务器配置一般又不想引入 Node 构建链路那 layui2.5.6 配 ThinkPHP6.0.2 这套组合反而更省心。这份资源就是一个基于 layui2.5.6 和 tp6.0.2 搭建的权限管理后台涵盖了菜单管理、角色分配、节点权限控制、管理员账号维护这些后台系统最核心的模块。它适合两类人一是刚接触 TP6 想找一个完整项目练手的开发者二是手头有中小型管理系统需求、不想在构建工具上耗时间的团队。整个项目没有花哨的前端工程化服务端渲染加 layui 的表格和表单组件改起来直接、跑起来快。2. 技术选型与目录结构为什么是 layui 加 TP6 而不是前后端分离2.1 这套组合解决了什么问题先说说为什么这个项目会选择 layui2.5.6 而不是 Vue 或者 React。核心原因在于后台管理系统的本质——它不需要复杂的交互状态管理大部分页面就是表格、表单、弹窗三件套。layui 的 table 模块自带分页、排序、筛选form 模块自带校验和提交弹层用 layer 一行代码就能调起来。用 layui 写一个 CRUD 页面代码量可能只有 Vue 方案的三分之一。而 TP6.0.2 作为服务端框架提供了路由、中间件、数据库 ORM、模板引擎这些后台系统必需的能力它的中间件机制特别适合做权限拦截——每个请求进来先过权限中间件没权限直接返回 403逻辑清晰。另一个现实考量是部署。前后端分离项目需要单独部署前端静态资源配置 Nginx 反向代理处理跨域问题。而这个项目把 layui 的静态文件直接放在 public/static 目录下TP6 的模板引擎渲染页面一个 Nginx 站点就能跑起来。对于只有一两台服务器的中小团队来说运维成本低很多。2.2 目录结构拆解项目遵循 TP6 默认的多应用模式目录结构大致如下app/ ├── admin/ # 后台管理应用 │ ├── controller/ # 控制器 │ │ ├── Index.php # 后台首页 │ │ ├── Login.php # 登录控制器 │ │ ├── Menu.php # 菜单管理 │ │ ├── Role.php # 角色管理 │ │ ├── Admin.php # 管理员管理 │ │ └── Node.php # 节点权限管理 │ ├── model/ # 模型层 │ │ ├── MenuModel.php │ │ ├── RoleModel.php │ │ ├── AdminModel.php │ │ └── NodeModel.php │ ├── middleware/ # 中间件 │ │ ├── Auth.php # 登录鉴权中间件 │ │ └── CheckAuth.php # 权限校验中间件 │ ├── validate/ # 验证器 │ └── view/ # 模板文件 │ ├── index/ │ ├── login/ │ ├── menu/ │ ├── role/ │ └── admin/ ├── common.php # 公共函数 ├── event.php # 事件定义 ├── middleware.php # 全局中间件 └── provider.php # 容器绑定 config/ ├── app.php # 应用配置 ├── database.php # 数据库配置 ├── session.php # Session 配置 └── template.php # 模板配置 public/ ├── static/ │ ├── layui/ # layui2.5.6 静态资源 │ ├── css/ │ └── js/ ├── index.php # 入口文件 └── .htaccess route/ └── app.php # 路由定义这个结构里最值得关注的是app/admin/middleware/下的两个中间件。Auth.php负责检查用户是否登录没登录就跳转到登录页CheckAuth.php负责检查当前登录用户是否有权限访问当前节点。两个中间件分工明确一个管你是谁一个管你能干什么。2.3 数据库表设计权限系统的核心在于几张关联表的设计。项目里主要涉及这几张表表名用途关键字段admin管理员账号id, username, password, role_id, statusrole角色表id, role_name, description, statusmenu菜单表id, pid, title, icon, url, sortnode节点权限表id, pid, node_name, controller, actionrole_node角色节点关联role_id, node_id角色和节点之间是多对多关系通过 role_node 中间表关联。管理员通过 role_id 绑定角色登录后根据角色查出对应的节点权限存到 Session 里。每次请求进来CheckAuth 中间件比对当前控制器和方法是否在权限列表里。注意node 表里的 controller 和 action 字段要和实际控制器类名、方法名对应大小写敏感。TP6 默认的控制器命名是首字母大写的驼峰比如Menu.php对应menu控制器方法名index对应index操作。3. 权限校验链路从登录到节点拦截的完整实现3.1 登录逻辑与 Session 写入登录是整个权限系统的入口。用户提交账号密码后服务端验证通过把管理员信息和权限节点写入 Session。先看登录控制器的核心代码// app/admin/controller/Login.php namespace app\admin\controller; use app\admin\model\AdminModel; use app\admin\model\NodeModel; use think\facade\Session; use think\facade\View; class Login { // 登录页面渲染 public function index() { return View::fetch(login/index); } // 处理登录请求 public function doLogin() { $username input(post.username/s, ); $password input(post.password/s, ); if (empty($username) || empty($password)) { return json([code 0, msg 账号或密码不能为空]); } $admin AdminModel::where(username, $username) -where(status, 1) -find(); if (!$admin) { return json([code 0, msg 账号不存在或已被禁用]); } // 密码校验数据库存的是 password_hash 加密后的值 if (!password_verify($password, $admin-password)) { return json([code 0, msg 密码错误]); } // 写入 Session Session::set(admin_id, $admin-id); Session::set(admin_name, $admin-username); Session::set(role_id, $admin-role_id); // 查出该角色的所有权限节点 $nodeIds \think\facade\Db::name(role_node) -where(role_id, $admin-role_id) -column(node_id); $nodes NodeModel::whereIn(id, $nodeIds)-select()-toArray(); $authList []; foreach ($nodes as $node) { $authList[] strtolower($node[controller] . / . $node[action]); } Session::set(auth_list, $authList); return json([code 1, msg 登录成功]); } }这段代码有几个关键点。第一密码用password_hash和password_verify处理这是 PHP 内置的加密方案比 md5 安全得多。第二登录成功后不仅存了管理员 ID还把该角色的权限节点列表查出来存进 Session这样后续每次请求不用再查数据库。第三权限列表的格式是controller/action的小写字符串方便后续比对。参数说明input(post.username/s)里的/s是 TP6 的变量修饰符表示强制转为字符串类型防止数组注入。status字段用来控制账号是否启用被禁用的账号即使密码正确也无法登录。3.2 鉴权中间件的注册与执行TP6 的中间件需要在应用目录下注册。项目里有两个中间件Auth负责登录检查CheckAuth负责权限检查。注册方式如下// app/admin/middleware.php return [ // 登录鉴权中间件全局生效 \app\admin\middleware\Auth::class, // 权限校验中间件全局生效 \app\admin\middleware\CheckAuth::class, ];中间件的执行顺序和注册顺序一致先执行 Auth 再执行 CheckAuth。Auth 中间件的实现// app/admin/middleware/Auth.php namespace app\admin\middleware; use think\facade\Session; use think\Response; class Auth { public function handle($request, \Closure $next) { $controller $request-controller(); $action $request-action(); // 登录页和登录接口不需要鉴权 $whiteList [Login/index, Login/doLogin, Login/captcha]; if (in_array($controller . / . $action, $whiteList)) { return $next($request); } // 检查 Session 中是否有管理员 ID if (!Session::get(admin_id)) { // AJAX 请求返回 JSON普通请求跳转登录页 if ($request-isAjax()) { return json([code 401, msg 登录已过期请重新登录]); } return redirect(/admin/login/index); } return $next($request); } }这里有个细节值得注意白名单用的是Login/index这种格式和 Session 里存的权限列表格式保持一致。判断 AJAX 请求是为了区分页面跳转和接口调用——如果是 layui 的表格异步请求返回 JSON 让前端处理跳转如果是直接访问页面服务端直接 302 重定向。3.3 节点权限校验与无权限处理CheckAuth 中间件是权限系统的核心它决定了当前登录用户能否访问目标节点// app/admin/middleware/CheckAuth.php namespace app\admin\middleware; use think\facade\Session; class CheckAuth { public function handle($request, \Closure $next) { $controller strtolower($request-controller()); $action strtolower($request-action()); $node $controller . / . $action; // 白名单直接放行 $whiteList [login/index, login/dologin, login/captcha, index/index]; if (in_array($node, $whiteList)) { return $next($request); } $authList Session::get(auth_list) ?: []; // 超级管理员拥有所有权限 if (Session::get(admin_id) 1) { return $next($request); } if (!in_array($node, $authList)) { if ($request-isAjax()) { return json([code 403, msg 没有操作权限]); } return view(public/403); } return $next($request); } }这段代码里$request-controller()返回的是控制器名TP6 默认返回的是首字母大写的驼峰形式所以需要strtolower转成小写再比对。超级管理员的判断用的是admin_id 1这是项目约定的超级管理员账号不受权限节点限制。提示如果新增了控制器或方法记得在 node 表里添加对应的节点记录并在角色管理里给对应角色勾选权限否则非超管账号访问会直接 403。4. 菜单与角色管理layui 表格和树形组件的实战4.1 菜单管理的树形结构渲染菜单管理是后台系统里比较有代表性的模块因为它涉及树形结构的展示和操作。项目里菜单表用pid字段实现无限级分类前端用 layui 的 treeTable 或者 treetable 插件渲染。先看服务端的数据组装// app/admin/controller/Menu.php namespace app\admin\controller; use app\admin\model\MenuModel; use think\facade\View; class Menu { public function index() { return View::fetch(menu/index); } // 获取菜单树形数据 public function getTree() { $list MenuModel::order(sort, asc)-select()-toArray(); $tree $this-buildTree($list, 0); return json([code 0, msg , data $tree]); } // 递归构建树形结构 private function buildTree($list, $pid 0) { $tree []; foreach ($list as $item) { if ($item[pid] $pid) { $children $this-buildTree($list, $item[id]); if (!empty($children)) { $item[children] $children; } $tree[] $item; } } return $tree; } }buildTree方法是一个标准的递归实现从 pid 为 0 的顶级菜单开始逐层往下找子节点。返回的数据结构里有子节点的菜单会带上children字段layui 的 treeTable 组件识别这个字段后会自动渲染展开箭头。前端模板里layui 的表格初始化配置大致是这样// public/static/js/menu.js layui.use([treeTable, form, layer], function () { var treeTable layui.treeTable; var form layui.form; var layer layui.layer; // 渲染树形表格 treeTable.render({ elem: #menuTable, url: /admin/menu/getTree, tree: { customName: { name: title, pid: pid, children: children }, view: { expandAll: true, showIcon: true } }, cols: [[ { field: title, title: 菜单名称, width: 200 }, { field: url, title: 路由地址, width: 200 }, { field: icon, title: 图标, width: 80, templet: function (d) { return d.icon ? i classlayui-icon d.icon /i : ; }}, { field: sort, title: 排序, width: 80 }, { title: 操作, width: 200, templet: function (d) { return a classlayui-btn layui-btn-xs lay-eventedit编辑/a a classlayui-btn layui-btn-xs layui-btn-danger lay-eventdel删除/a; }} ]] }); });这里tree.customName配置告诉 layui 哪个字段是名称、哪个是父级标识、哪个是子节点集合。expandAll: true让表格默认展开所有层级。操作列用templet自定义渲染给每行加上编辑和删除按钮通过lay-event绑定事件。4.2 角色分配权限的交互实现角色管理里最核心的功能是给角色分配权限节点。项目里用 layui 的 tree 组件展示节点树勾选后提交到服务端保存。服务端需要把节点数据组装成树形结构返回// app/admin/controller/Role.php public function getNodeTree() { $roleId input(get.role_id/d, 0); // 查出所有节点 $nodes \app\admin\model\NodeModel::order(sort, asc)-select()-toArray(); // 查出该角色已分配的节点 $checkedIds []; if ($roleId) { $checkedIds \think\facade\Db::name(role_node) -where(role_id, $roleId) -column(node_id); } $tree $this-buildNodeTree($nodes, 0); return json([ code 0, data $tree, checkedIds $checkedIds ]); } private function buildNodeTree($list, $pid 0) { $tree []; foreach ($list as $item) { if ($item[pid] $pid) { $node [ id $item[id], title $item[node_name], children $this-buildNodeTree($list, $item[id]) ]; $tree[] $node; } } return $tree; }返回的数据里data是完整的节点树checkedIds是该角色已经勾选的节点 ID 数组。前端拿到数据后用 layui 的 tree 组件渲染并设置默认勾选状态// public/static/js/role.js layui.use([tree, form, layer], function () { var tree layui.tree; var form layui.form; var layer layui.layer; // 打开权限分配弹窗 function openAuthDialog(roleId) { $.get(/admin/role/getNodeTree, { role_id: roleId }, function (res) { tree.render({ elem: #nodeTree, data: res.data, showCheckbox: true, id: nodeTree, onlyIconControl: true, oncheck: function (obj) { // 勾选变化时的回调可以在这里做实时保存 } }); // 设置默认勾选 tree.setChecked(nodeTree, res.checkedIds); layer.open({ type: 1, title: 分配权限, area: [500px, 600px], content: $(#authDialog).html(), btn: [保存, 取消], yes: function (index) { var checkedData tree.getChecked(nodeTree); var ids []; // 递归提取所有勾选的节点 ID function extractIds(nodes) { nodes.forEach(function (node) { ids.push(node.id); if (node.children node.children.length) { extractIds(node.children); } }); } extractIds(checkedData); $.post(/admin/role/saveAuth, { role_id: roleId, node_ids: ids.join(,) }, function (res) { if (res.code 1) { layer.msg(保存成功); layer.close(index); } else { layer.msg(res.msg); } }); } }); }); } });这里有个容易踩坑的地方layui 的 tree 组件getChecked方法返回的是树形结构不是扁平的 ID 数组。所以需要写一个递归函数extractIds把所有层级的 ID 都提取出来。另外tree.setChecked必须在tree.render之后调用否则不生效。服务端保存权限的接口public function saveAuth() { $roleId input(post.role_id/d, 0); $nodeIds input(post.node_ids/s, ); if (!$roleId) { return json([code 0, msg 参数错误]); } $ids array_filter(explode(,, $nodeIds)); // 先删除旧权限 \think\facade\Db::name(role_node)-where(role_id, $roleId)-delete(); // 批量插入新权限 if (!empty($ids)) { $data []; foreach ($ids as $nodeId) { $data[] [role_id $roleId, node_id $nodeId]; } \think\facade\Db::name(role_node)-insertAll($data); } return json([code 1, msg 保存成功]); }保存逻辑用的是先删后插策略简单直接。对于权限节点数量不大的场景这种方式的性能完全够用。如果节点数量上千可以考虑用 diff 方式只更新变化的部分。5. 避坑与常见问题部署和调试中容易翻车的地方5.1 伪静态配置导致路由 404现象项目部署到 Nginx 后除了首页其他页面全部 404但本地用 PHP 内置服务器跑正常。原因TP6 依赖 URL 重写Nginx 默认不会把请求转发到 index.php。本地php think run启动的服务器内置了重写规则所以没问题。解决在 Nginx 站点配置里加上重写规则location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } }或者在 public 目录下放一个.htaccess文件Apache 环境内容为IfModule mod_rewrite.c RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [L,EPATH_INFO:$1] /IfModule5.2 Session 跨域丢失导致登录状态无法保持现象登录接口返回成功但后续请求一直提示未登录Session 里取不到 admin_id。原因TP6 默认的 Session 是文件存储如果前端请求的域名和接口域名不一致或者请求没有携带 CookieSession ID 就无法传递。解决确认前端请求时withCredentials为 true服务端配置 CORS 允许携带凭证。如果还是不行检查config/session.php里的domain配置确保和实际访问域名一致。另外 layui 的表格请求默认不带 Cookie需要在table.render的request配置里加上xhrFields: { withCredentials: true }。5.3 权限节点大小写不匹配导致 403现象明明在角色里勾选了某个菜单的权限但访问时还是提示没有权限。原因node 表里存的 controller 和 action 字段大小写和实际请求的不一致。TP6 的$request-controller()返回的是首字母大写的驼峰而数据库里可能存的是全小写。解决统一在中间件里做strtolower处理同时确保 node 表里的数据也统一为小写。可以在添加节点时自动转小写或者在查询时用LOWER()函数处理。5.4 layui 表格分页参数不生效现象layui 表格请求带了 page 和 limit 参数但服务端返回的数据没有分页效果。原因TP6 的paginate()方法默认读取的是page参数但 layui 默认传的是page和limit。如果服务端用的是paginate()需要手动指定每页数量。解决在控制器里这样处理$page input(get.page/d, 1); $limit input(get.limit/d, 10); $list MenuModel::order(sort, asc)-page($page, $limit)-select(); $total MenuModel::count(); return json([code 0, msg , count $total, data $list]);注意返回的 JSON 结构里count是总记录数data是当前页数据这是 layui 表格默认期望的格式。5.5 密码修改后旧 Session 未失效现象管理员修改了密码但已经登录的会话仍然有效不需要重新登录。原因Session 里存的是 admin_id密码修改后没有清除 Session。解决在修改密码的接口里修改成功后调用Session::clear()清除当前会话然后返回让前端跳转到登录页。如果要做更严格的控制可以在 admin 表里加一个token_version字段每次修改密码递增中间件里比对版本号。6. 二次开发改造从能跑到好用的几个关键调整拿到这个项目后如果只是跑起来看看那太浪费了。我一般会先做几件事让它更贴合实际业务。第一件是给权限节点加缓存。现在每次登录都要查一遍 role_node 和 node 表如果角色多、节点多登录接口会变慢。改造方式是在config/cache.php里配置 Redis 或者文件缓存把权限列表按 role_id 缓存起来角色权限变更时清除对应缓存。代码改动不大在Login::doLogin里把查库换成Cache::remember(auth_role_ . $admin-role_id, function() use ($admin) { ... }, 3600)就行。第二件是给操作日志加个记录。后台系统最怕的就是出了问题找不到是谁操作的。可以在 CheckAuth 中间件里加一段逻辑对于 POST 请求把操作人、控制器、方法、请求参数、IP、时间写进一张admin_log表。注意请求参数里的密码字段要过滤掉别把敏感信息记进去。这个改动对现有代码侵入很小中间件里加十几行就够了。第三件是菜单的图标选择器。layui 自带了一套图标字体但让运营人员手动输入图标类名不太现实。可以在菜单编辑页面加一个图标选择弹窗把 layui 的图标列表渲染出来点击即选中。实现方式是在模板里加一个隐藏的 input 存图标类名旁边放个按钮触发 layer.open 弹出一个图标网格点击某个图标后把类名写回 input 并关闭弹窗。第四件是登录加验证码。项目里虽然预留了Login/captcha的白名单但验证码功能需要自己接。TP6 可以用topthink/think-captcha扩展安装后在登录页模板里加{:captcha_img()}输出验证码图片登录接口里用captcha_check()校验。注意验证码的 Session 存储和登录 Session 是同一个别配错了驱动。最后说一个我踩过的坑这个项目的权限控制粒度是控制器方法级别的但实际业务里经常需要更细的控制比如同一个方法里不同用户能看到不同的数据行。这种数据级权限没法靠中间件解决需要在模型层或者服务层单独处理。我的做法是在 admin 表里加一个data_scope字段值为 1 表示全部数据、2 表示本部门数据、3 表示仅本人数据然后在查询构造器里根据这个字段拼接 where 条件。这个改造涉及面比较广建议先把基础权限跑通再做。从那以后我每次接手这类权限后台都会先把节点表和角色关联表的数据导出来看一眼确认权限树的层级和实际菜单是否对得上再开始改代码。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑