ThinkPHP6.0.2+Layui2.5.6权限后台部署与鉴权避坑
简介面向 PHP 后台开发初学者与二次开发者的权限管理项目模板将 layui2.5.6 前端组件库与 ThinkPHP6.0.2 后端框架整合重点解决登录鉴权、角色分配与菜单权限控制等需求可用于快速搭建管理后台原型也适合作为课程设计参考。压缩包共 1098 个文件、约 6.1MB主体是 481 个 php 源码与 139 个 js 脚本配合 30 个 css、36 个 html 页面构成前后端交互骨架另有 gif、png、svg 等界面素材以及 md 使用说明、json 与 yml 配置、sql 建表文件目录涵盖框架核心、公共静态资源与依赖描述等层次并按 Composer 方式管理依赖便于按模块阅读与调试。目前已有 2285 人学习下载。借助该案例可了解 RBAC 权限模型的落地方式、.env 环境配置的切换办法以及 layui 组件与后端接口的对接思路从而理清前后端协同开发的基本路径。1. 装之前先定版本PHP 7.4 与 Composer 2 是 tp6.0.2 的舒适区把 layui2.5.6 tp6.0.2 权限管理后台从压缩包跑到能正常登录卡住人的从来不是权限逻辑而是版本。我见过最典型的一次是把项目丢进一台预装 PHP 8.1 的机器php think run倒是起来了一进登录页就白屏runtime/log里只有一行看不懂的反射报错排查了半天才发现是运行环境的问题跟代码一行关系都没有。这套 tp6 layui 组合在后台管理场景里太常见了左侧菜单、角色分配、节点鉴权、列表增删改查一套骨架能撑起大半个中后台。但它对运行环境有自己的脾气PHP 版本高了低了我都不建议PHP 7.4 加 Composer 2.x 是踩过坑之后最省事的一档数据库用 MySQL 5.7 或 8.0 都行。这套东西适合手上有后台需求、想拿一套能跑通的骨架改的人如果你的目标是用 PHP 8.3 那一套那这篇的路径不适用别硬套。2. 搭底座PHP 7.4 扩展、Composer 2 与 tp6.0.2 骨架拉取2.1 PHP 7.4 与六个必开扩展tp6.0.2 的依赖声明里写的是 PHP 7.1但真正让人翻车的往往不是版本号而是扩展。我一般先跑两条命令把底摸清楚而不是等页面报 500 再回头猜。# 看大版本以及实际加载的是哪一个 php.ini php -v php --ini # 一次性筛出 tp6 从启动到连库、渲染、写日志要用的扩展 php -m | grep -Ei openssl|mbstring|pdo|mysql|curl|fileinfo|tokenizer|xml|jsonphp -v决定后面所有讨论的前提php --ini拿到的才是真正生效的 ini 路径很多「我明明改了 php.ini 却没反应」的问题都是改到了另一个版本目录下的配置文件。php -m列的是已启用模块上面这几个是必经之路openssl 供 Composer 与加密函数使用mbstring 处理中文字符串截断pdo_mysql 负责连库fileinfo 判断上传文件真实类型tokenizer 供模板解析xml 与 json 则是框架内部序列化的常客。Linux 下补扩展最省事的是包管理器把版本号换成你自己装的sudo apt install php7.4-mbstring php7.4-xml php7.4-curl php7.4-mysql php7.4-fileinfo -y sudo systemctl restart php7.4-fpm # 走 fpm 的必须重启才生效Windows 下打开 php.ini去掉对应行的分号并把 extension_dir 指到 ext 目录extension_dir D:/php7.4/ext extensionopenssl extensionmbstring extensionpdo_mysql extensionfileinfo extensioncurl改完别只看 ini 里有没有那行回到终端用php -m复查一遍——扩展加载失败时 PHP 只在启动日志里留一句警告页面上是看不出来的。顺手再看一眼disable_functions部分集成环境默认把putenv、proc_open关掉Composer 装依赖时会直接中断报错信息还挺隐晦。2.2 Composer 2 安装与镜像配置Composer 走的是 HTTPS 拉包机器时间不准、CA 证书过期都会失败先把这两样确认掉再装。# 官方安装脚本装到当前目录后移到全局 php -r copy(https://getcomposer.org/installer, composer-setup.php); php composer-setup.php --install-dir/usr/local/bin --filenamecomposer php -r unlink(composer-setup.php); composer -V # 确认是 2.x composer diagnose # 体检网络、证书、缓存目录一次看全--install-dir与--filename决定最终的可执行文件名放在/usr/local/bin下可以直接用composer而不是./composer.phar。composer diagnose这一步我强烈建议每次都跑它会把网络连通性、证书、代理设置、缓存目录权限全列出来比出问题后一个个猜要快得多。拉包慢是常态配全局镜像把地址换成你网络环境下实际可用的那个composer config -g repo.packagist composer https://mirrors.example.com/composer/ composer config -g --list | grep repos # 确认写进去了这条命令改的是全局配置文件不动项目里的 composer.json。好处是换机器、换项目都不用重复配坏处是镜像偶尔同步滞后装到一半提示某个包「not found」先把镜像关掉再试一次能立刻判断是源的问题还是包名写错了。2.3 拉 tp6.0.2 骨架并跑起来版本号一定要写死create-project不带约束会给你拉最新的 6.x 小版本跟 layui2.5.6 那套模板写法对不上。# 目录名自定版本号锁死在 6.0.2 composer create-project topthink/think tpadmin 6.0.2 cd tpadmin cp .example.env .env # 环境变量文件后面连库要用 php think run -H 0.0.0.0 -p 8000-H 0.0.0.0是为了让容器外或局域网内能访问只在本机调试用127.0.0.1就行。启动后访问http://你的IP:8000出现框架默认的欢迎页说明骨架这一层是通的。如果这里就白屏先看runtime/log下当天的日志文件再看 PHP 错误日志两边都没有就往扩展和disable_functions上查。拉下来之后目录长这样几个位置要提前记住app/放业务代码config/放配置public/是唯一的 Web 入口route/是路由定义runtime/是缓存与日志。vendor/不提交版本库换机器重新composer install即可。2.4 public 入口与伪静态新手最容易栽的一步把站点根目录指到了项目根而不是public。后果是.env、composer.json全暴露在浏览器里属于安全事故级别的问题。Nginx 的最小可用配置server { listen 80; server_name admin.local; root /data/www/tpadmin/public; # 必须指到 public index index.php; location / { try_files $uri $uri/ /index.php$is_args$args; # 去掉 index.php 的关键 } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~ /\.(env|git) { deny all; # 环境文件与版本库目录禁止访问 } }try_files那行是伪静态的核心找不到实体文件就统一交给index.php路由才能接管。最后那段 deny 是兜底就算根目录配错了.env也读不到。Apache 环境更简单public目录下框架自带的.htaccess直接生效只需要确认mod_rewrite已开、AllowOverride All已配。3. 接前端layui2.5.6 静态资源落位与后台布局模板3.1 layui2.5.6 放哪目录带版本号静态资源我只认一种放法public/static/下面按「库名/版本号」分层把 2.5.6 写进路径里。public/static/ └── layui/ └── 2.5.6/ ├── layui.js ├── css/layui.css ├── font/ └── modules/这样做的好处很直接以后要试 2.6 或更高版本新开一个目录并行放改模板里的引用路径就能灰度不用覆盖旧文件。覆盖式升级在后台项目里是血泪教训一旦新版本某个组件的默认样式变了线上表格立刻变形想回退还得翻备份。模板里的引用保持相对路径别写死域名link relstylesheet href/static/layui/2.5.6/css/layui.css script src/static/layui/2.5.6/layui.js/scriptmodules/整个目录必须一起拷layui 是按需异步加载模块的少一个文件页面只会在控制台报一句 404界面上什么都不显示属于典型的黑匣子问题。3.2 多应用模式与模板引擎两个包别忘tp6 的骨架默认是单应用也不带模板引擎这两样都要额外装。这是我认为整套安装里最容易漏的一步。composer require topthink/think-multi-app # 多应用app/admin、app/index 这种结构 composer require topthink/think-view # 模板引擎{extend}、{block} 才可用装完think-multi-app之后访问方式从/index/index变成/admin/index/indexapp/admin/就是一个独立应用。think-view提供的是view()助手函数和模板继承能力没有它return view()会直接抛方法不存在的异常。应用目录按这个结构铺app/ ├── admin/ │ ├── controller/ │ │ └── Index.php │ ├── middleware/ │ │ └── Auth.php │ ├── middleware.php # 应用级中间件注册 │ └── view/ │ ├── public/ │ │ └── layout.html # 公共骨架 │ └── index/ │ └── index.html └── common.php控制器里渲染视图只需要一句namespace app\admin\controller; class Index { // 对应 app/admin/view/index/index.html public function index() { return view(); } }view()不带参数时按「控制器名/方法名」自动找模板规则是view/控制器/方法.html。想指定别的模板就传路径view(public/main)这种写法我用得最多。3.3 后台主框架layout 继承与 iframe 菜单后台布局有两种做法iframe 版和单页版。前者菜单点击换 iframe 的 src每个页面互相独立后者用前端路由整站一个 HTML。layui2.5.6 这套生态里iframe 版落地最快出问题也最好定位。公共骨架layout.html!DOCTYPE html html head meta charsetutf-8 title{block nametitle}后台管理{/block}/title link relstylesheet href/static/layui/2.5.6/css/layui.css /head body {include filepublic/header /} div classlayui-body {block namecontent}{/block} /div script src/static/layui/2.5.6/layui.js/script {block namescript}{/block} /body /html子模板只需要三行{extend namepublic/layout /} {block nametitle}角色管理{/block} {block namecontent} table idroleTable lay-filterroleTable/table {/block}{extend}必须写在第一行前面连空行都不能有否则模板解析会把它当普通文本输出到页面上。{block}的名字要和父模板一一对应多写一个不会报错只是永远不渲染——这类问题没有报错提示只能靠肉眼比对。3.4 接口返回格式跟 layui table 对齐前端表格直接决定了后端返回的数据结构这一层不统一后面每个列表页都要单独写一遍解析。// app/common.php全局可调 function json_table($list, $total, $code 0, $msg ) { // layui table 默认读 code / msg / count / data 四个字段 return json([ code $code, msg $msg, count $total, data $list, ]); }code为 0 表示成功非 0 时前端弹msgcount是总条数分页组件靠它算页数data是当前页数据。分页参数 layui 默认传page和limit后端直接接$page (int) $request-param(page, 1); $limit (int) $request-param(limit, 10); $total Db::name(role)-count(); $list Db::name(role)-page($page, $limit)-select()-toArray(); return json_table($list, $total);page($page, $limit)的两个参数顺序是页码在前、每页条数在后写反了不会报错只是数据错位。toArray()别漏tp6 的查询结果默认是数据集对象直接塞进 json 会变成嵌套结构前端表格拿不到字段。4. 落权限TP6 的四张表、.env 连库与中间件鉴权4.1 四张核心表管理员、角色、节点、关联权限后台的表结构不用复杂四张够用。管理员挂角色角色挂节点节点就是「哪个控制器下的哪个方法」。-- 管理员表 CREATE TABLE admin ( id int unsigned NOT NULL AUTO_INCREMENT, username varchar(32) NOT NULL COMMENT 登录名, password varchar(255) NOT NULL COMMENT password_hash 结果, role_id int unsigned NOT NULL DEFAULT 0 COMMENT 角色ID, status tinyint NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, create_time int unsigned NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 角色表 CREATE TABLE role ( id int unsigned NOT NULL AUTO_INCREMENT, name varchar(32) NOT NULL, status tinyint NOT NULL DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 节点表pid 做无限级菜单 CREATE TABLE node ( id int unsigned NOT NULL AUTO_INCREMENT, pid int unsigned NOT NULL DEFAULT 0, title varchar(64) NOT NULL COMMENT 菜单名, module varchar(32) NOT NULL DEFAULT admin, controller varchar(64) NOT NULL, action varchar(64) NOT NULL, sort int NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_pid (pid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 角色与节点多对多 CREATE TABLE role_node ( role_id int unsigned NOT NULL, node_id int unsigned NOT NULL, PRIMARY KEY (role_id,node_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;controller和action统一小写存校验时也统一转小写比对大小写混着存是权限「配了却不生效」的头号原因。role_node用联合主键天然防重复授权比加自增 id 再补唯一索引省事。超级管理员我一般不走关联表直接判role_id 1放行。理由很实在节点是动态增删的超管每次加功能都要回来补授权早晚会漏。4.2 .env 与 config/database.php两个入口别打架tp6 的数据库配置有两处.env和config/database.php。规则是配置文件里用env()取值.env有就用.env的没有就用第二个参数兜底。[DATABASE] TYPE mysql HOSTNAME 127.0.0.1 DATABASE tpadmin USERNAME tpadmin PASSWORD 换成你的密码 HOSTPORT 3306 CHARSET utf8mb4 DEBUG true对应的config/database.php关键几行return [ type env(database.type, mysql), hostname env(database.hostname, 127.0.0.1), database env(database.database, ), username env(database.username, root), password env(database.password, ), hostport env(database.hostport, 3306), charset env(database.charset, utf8mb4), ];env(database.hostname)里的点号对应.env中的[DATABASE]段和HOSTNAME键大小写不敏感。改了.env之后如果没生效多半是runtime/下留了旧缓存php think clear清一次再刷新。生产环境记得把DEBUG关掉APP_DEBUG false否则报错页会把连接信息直接打到浏览器上。建库的时候字符集一步到位CREATE DATABASE tpadmin DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;用utf8也能跑但存 emoji 和部分生僻字会截断后台里填个备注就莫名其妙少半截返工成本比一开始就选对高得多。4.3 登录与会话密码哈希和 session 落点密码不存明文也不自己写 md5 加盐用 PHP 自带的password_hash验证用password_verify将来换算法也不用改表结构。public function login(Request $request) { $username $request-param(username, ); $password $request-param(password, ); $admin Db::name(admin)-where(username, $username)-find(); // 用户不存在与密码错误返回同一句提示避免被枚举账号 if (!$admin || !password_verify($password, $admin[password])) { return json([code 1, msg 账号或密码错误]); } if ((int) $admin[status] ! 1) { return json([code 1, msg 账号已禁用]); } // 只写必要字段进 session别把整行数据丢进去 session(admin, [ id $admin[id], username $admin[username], role_id $admin[role_id], ]); return json([code 0, msg 登录成功]); }session 驱动在config/session.php里改单机部署用默认的文件驱动就够多机才需要换到 redis 或数据库。文件驱动的 session 存runtime/session/目录权限不对会表现为「登录成功但一刷新就掉」这是权限问题的伪装排查时先看目录属主。退出登录把 session 清干净public function logout() { session(null); // 清空当前会话数据 return redirect(/admin/login/index); }4.4 中间件做鉴权把判断收到一处每个控制器方法里写一遍权限判断是这套后台最常见的退化方式——加一个方法忘了加判断就是一个越权入口。正确做法是应用级中间件统一拦。namespace app\admin\middleware; use think\facade\Db; class Auth { public function handle($request, \Closure $next) { $admin session(admin); // 未登录直接跳登录页 if (!$admin) { return redirect(/admin/login/index); } // 超管放行不做节点比对 if ((int) $admin[role_id] 1) { return $next($request); } // 当前请求对应的节点标识统一小写 $node strtolower($request-controller() . / . $request-action()); $allow Db::name(role_node) -alias(rn) -join(node n, n.id rn.node_id) -where(rn.role_id, $admin[role_id]) -whereRaw(LOWER(CONCAT(n.controller, /, n.action)) :node, [node $node]) -count(); if (!$allow) { return json([code 403, msg 无访问权限], 403); } return $next($request); } }注册在app/admin/middleware.php登录接口本身要排除return [ // 除登录相关方法外全部走鉴权 \app\admin\middleware\Auth::class [except [login/index, login/check, login/logout]], ];except里写的是「控制器/方法」的小写形式写错大小写会导致该放行的方法被拦下来表现是登录页一直跳回自己。whereRaw里的CONCAT是为了把两列拼成一个标识再比避免先查出全部节点再在 PHP 里 foreach节点上百条之后差异很明显。鉴权中间件返回 JSON 还是跳转取决于请求类型。我一般在这里判一下$request-isAjax()接口请求返回 403 JSON页面请求跳登录页两套前端都能正常处理。5. 避坑排查这套组合最常翻车的五个点5.1 页面白屏日志里什么都没有现象访问后台任意页面返回 500浏览器空白runtime/log里当天的文件是空的或者只有框架启动那几行。原因tp6 的异常处理在调试模式下才把详细堆栈写进日志APP_DEBUG为 false 时异常被吞成一句「页面错误」。另外 PHP 自己的display_errors关掉后fatal error 也不会显示在页面上。解决临时把.env里的APP_DEBUG打开同时确认log配置的level包含error。如果还是没内容直接看 PHP-FPM 的 error log/var/log/php7.4-fpm.log或 nginx 的 error_log语法错误、内存超限这类问题只出现在那边。5.2 layui 表格一直显示「请求异常」现象列表页加载后表格区域只显示「请求异常」四个字Network 面板里接口是 200返回的 JSON 看着也正常。原因layui table 默认解析code / msg / count / data返回体里少一个字段或者code不是 0前端就按失败处理。另一种情况是接口返回了 HTML比如被中间件重定向到登录页data解析不出来。解决先在 Network 里点开 Response 原样看是 JSON 还是 HTML。是 HTML 就查中间件的except有没有放行这个方法是 JSON 就核对四个字段是否齐全count必须是数字不能是字符串data必须是数组不能是对象。5.3 权限配了不生效改了菜单没反应现象后台给角色勾了新节点保存成功重新登录该角色还是看不到菜单或者菜单出来了点进去 403。原因一是节点表里controller、action存了大小写混合鉴权时统一转了小写比对不上二是runtime/下有旧的缓存文件三是菜单是登录时一次性写进 session 的改了库但 session 里还是老数据。解决查库确认两列全小写执行php think clear清缓存菜单别存 session每次渲染从库里读量为几十条时这点开销可以忽略。5.4 子目录部署后静态资源全 404现象项目放在https://域名/admin/这种子路径下页面能打开但样式全丢控制台一片 404请求路径变成了/admin/static/...之外的位置。原因模板里写的是/static/...这种以根开头的绝对路径浏览器会从域名根开始找不认子目录。解决模板里改用{:static_path()}之类的助手函数拼前缀或者在config/view.php里配一个tpl_replace_string把占位符统一替换成带前缀的路径。散落在几十个模板里的硬编码路径越早统一越省事。5.5 iframe 里登录态丢失现象主框架登录正常点左侧菜单进 iframe 页面内容区跳回了登录页。原因session 的 cookie 作用域或 SameSite 属性配置不当跨端口、跨子域时浏览器不带 cookie也可能是 session 存储目录权限不对写入失败后每次请求都是新会话。解决config/cookie.php里把domain配成实际域名不要留空又跨子域访问samesite设成lax然后检查runtime/session/的属主是否与 PHP-FPM 运行用户一致。6. 装完怎么验证四条链路依次打通才算跑通装完不等于跑通。我判断一套后台能不能交付是按四条链路依次走的前面不通就不往下走避免问题叠在一起分不清是谁的锅。第一条静态链路。浏览器直接访问/static/layui/2.5.6/layui.js返回 200 且 Content-Type 是application/javascript。返回 404 说明目录没放对返回 HTML 说明伪静态把静态文件也接管了try_files那行要检查。第二条模板链路。访问/admin/index/index页面源码里能看到 layui 的样式引用和渲染出来的菜单结构而不是一堆{block}原文。后者说明think-view没装或没生效。第三条数据链路。登录接口能返回code: 0且admin表里能查到session写入对应的记录。这一步过了说明.env、pdo_mysql、密码哈希三样都对。第四条权限链路。用两个角色对照这是最容易被跳过也最该做的一步验证项操作期望结果超管放行用 role_id1 的账号访问任意方法全部 200普通角色越权用只授权了角色列表的账号访问/admin/admin/index返回 403 JSON菜单渲染普通角色登录后的左侧菜单只出现已授权节点未登录拦截清 cookie 后直接访问后台首页跳转登录页四条都过再去做业务功能的增删改查。有一步没过就先停下别急着往下写代码——这一层没稳住后面每加一个功能都是在沙子上盖楼。从那以后我每次部署这套东西都会强制走一遍这四条链路尤其是权限那条的两角色对照。有回赶时间只测了超管上线第二天就被反馈普通运营能看到全部管理员列表回头补中间件、补节点、重新授权折腾的时间够把四条链路走十遍。希望帮到你。本文还有配套的精品资源点击获取