
简介本资源是一套基于PHP主流框架ThinkPHP6构建的后台管理框架源码面向有一定PHP基础、希望深入理解企业级后台开发与权限设计的开发者。项目采用前后端分离架构前后端通过API接口交互并集成RBAC权限管理模型通过角色关联用户与权限便于应对复杂多变的权限需求适合用于学习后台系统架构、权限控制与接口协作的实践场景。压缩包共599个文件约798KB以484个PHP源码文件为核心辅以25个Markdown说明、18个JSON配置、12个stub模板及yml、sql、env等配置与数据库脚本目录结构围绕think-admin主模块组织便于按模块阅读与二次开发。目前已有83人学习下载。通过该资源读者可获取一套结构完整的后台管理框架参考实现理解前后端分离下的接口设计与RBAC权限落地方式并借助配置与数据库脚本快速搭建本地运行环境为自身项目开发提供可复用的架构思路与排错参考。1. 从一张权限表说起ThinkPHP6 后台框架到底解决什么问题很多团队的后台管理系统最初都是“能跑就行”菜单写死在模板里权限判断散落在各个控制器前端页面和后端接口混在同一个目录。等到运营要加一个“只读财务”角色或者产品要求把管理端拆成独立 SPA改造成本立刻失控。ThinkPHP6 后台管理框架配合前后端分离与 RBAC 权限管理解决的正是这类问题它把“谁能看到哪个菜单、谁能调用哪个接口”从业务代码里抽出来变成可配置、可校验、可审计的一层基础设施。这套方案适合三类人一是要在一到两周内交付一个可用的运营后台又不想从零写登录、菜单、角色、日志的开发者二是已经用 ThinkPHP6 做 API但权限校验还停留在if ($user[role] admin)这种硬编码阶段的团队三是需要把后台前端交给独立前端同学维护后端只负责提供 JSON 接口的场景。下面按“先立住模型再动手复现最后排坑”的顺序展开中间会给出可直接抄的代码和参数说明。2. RBAC 模型怎么落到 ThinkPHP6 的表结构和中间件上2.1 五张核心表与字段设计RBAC 不是新概念但落到 ThinkPHP6 项目里表结构设计直接决定后面写代码顺不顺手。常见做法是五张表admin_user、role、permission、role_permission、user_role。不要一上来就搞“权限继承”“数据权限”先把功能权限跑通。表名关键字段说明admin_userid, username, password, salt, status后台用户密码用 password_hash 存储roleid, name, slug, status角色slug 用于代码里判断如 finance_readonlypermissionid, name, slug, type, parent_id, path权限项type 区分菜单/按钮/接口role_permissionrole_id, permission_id角色与权限多对多user_roleuser_id, role_id用户与角色多对多permission表里的type字段很关键菜单类型用于前端渲染侧边栏接口类型用于后端中间件校验。path存接口路由或前端路由例如admin/user/list。parent_id用来做菜单树避免前端自己拼层级。2.2 用中间件做接口级权限校验ThinkPHP6 的中间件是权限校验最自然的落点。不要在每个控制器方法里写checkAuth()那样一旦漏写就是越权漏洞。下面是一个可复现的权限中间件骨架?php // app/admin/middleware/CheckPermission.php namespace app\admin\middleware; use think\Request; use think\Response; use app\admin\model\AdminUser; class CheckPermission { public function handle(Request $request, \Closure $next) { $user $request-userInfo; // 由前置 JWT 中间件注入 if (!$user) { return json([code 401, msg 未登录], 401); } // 当前请求的权限标识约定为 控制器/方法 $slug strtolower($request-controller()) . / . strtolower($request-action()); // 超级管理员直接放行slug 在角色表里固定为 super_admin if (in_array(super_admin, $user[roles])) { return $next($request); } $permissions AdminUser::getPermissions($user[id]); // 返回 slug 数组 if (!in_array($slug, $permissions)) { return json([code 403, msg 无权限], 403); } return $next($request); } }逻辑说明先取用户再拼出当前请求的权限标识然后判断是否超管最后查用户权限集合。参数说明$request-controller()返回的是控制器名ThinkPHP6 默认不带Controller后缀所以拼出来的 slug 要和permission表里存的一致。如果项目用了多级控制器建议在permission.path里存完整路由而不是靠控制器名拼。AdminUser::getPermissions()的实现要注意缓存。每次请求都查三张表关联QPS 一上来数据库先扛不住。常见做法是用 Redis 缓存用户权限key 为admin:perm:{user_id}角色或权限变更时删掉对应 key。2.3 前后端分离下的登录态与菜单下发前后端分离后后端不再渲染菜单 HTML而是返回 JSON。登录接口返回 token前端存起来后续请求带在 header 里。菜单接口根据当前用户权限过滤permission表中typemenu的记录组装成树返回。?php // app/admin/controller/Auth.php public function login() { $data input(post.); $user AdminUser::where(username, $data[username])-find(); if (!$user || !password_verify($data[password], $user-password)) { return json([code 400, msg 账号或密码错误]); } $token Jwt::encode([uid $user-id, exp time() 7200]); return json([code 0, data [token $token]]); } public function menus() { $user request()-userInfo; $menus AdminUser::getMenus($user[id]); // 只返回 typemenu 且有权限的 return json([code 0, data $menus]); }参数说明token 有效期按项目安全要求设后台管理一般 2 小时到 8 小时。getMenus()里要按parent_id递归组装返回结构建议是{id, name, path, children}前端直接映射到路由和侧边栏。注意不要把没有权限的菜单也返回给前端再靠前端隐藏那是自欺欺人接口该拦的必须拦。3. 从零跑通一个最小可用的 ThinkPHP6 后台接口3.1 环境准备与项目初始化先确认 PHP 版本。ThinkPHP6 要求 PHP 7.2.5实际项目建议 7.4 或 8.08.1 以上部分扩展兼容性要自己测。用 Composer 创建项目composer create-project topthink/think tp6-admin cd tp6-admin composer require topthink/think-multi-app composer require firebase/php-jwt逻辑说明think-multi-app用于多应用模式把后台接口放在app/admin下和前台 API 隔离。firebase/php-jwt用来签发和校验 token。参数说明如果公司内网 Composer 慢配置国内镜像但不要用来源不明的镜像包。数据库配置在config/database.php建议用环境变量而不是硬编码。.env文件里写DATABASE_HOSTNAME、DATABASE_NAME等ThinkPHP6 会自动读取。注意.env不要提交到 Git。3.2 迁移文件与初始数据用 ThinkPHP 的迁移工具建表比手写 SQL 更可追溯composer require topthink/think-migration php think migrate:create CreateAdminTables生成的迁移文件里写五张表的up()方法。下面以permission表为例public function up() { $this-table(permission, [engine InnoDB]) -addColumn(name, string, [limit 50]) -addColumn(slug, string, [limit 100]) -addColumn(type, string, [limit 20, default menu]) -addColumn(parent_id, integer, [default 0]) -addColumn(path, string, [limit 200, default ]) -addColumn(create_time, integer, [default 0]) -create(); }逻辑说明type默认 menu接口权限单独标 api。parent_id默认 0 表示顶级菜单。参数说明slug要唯一建议加唯一索引避免重复权限项导致校验歧义。初始数据里至少插入一个超管角色和一个超管用户超管角色的 slug 固定为super_admin中间件里直接放行。3.3 接口联调与跨域处理前后端分离后前端跑在localhost:8080后端跑在localhost:8000跨域是第一个拦路虎。ThinkPHP6 可以用中间件统一处理?php // app/admin/middleware/Cors.php namespace app\admin\middleware; class Cors { public function handle($request, \Closure $next) { $header [ Access-Control-Allow-Origin $request-header(origin, *), Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS, Access-Control-Allow-Headers Authorization,Content-Type, Access-Control-Allow-Credentials true, ]; if ($request-method() OPTIONS) { return response(, 204, $header); } return $next($request)-header($header); } }逻辑说明预检请求直接返回 204正常请求在响应头里加 CORS 字段。参数说明Access-Control-Allow-Origin不要在生产环境用*配合Allow-Credentials: true浏览器会拒绝。生产环境应该校验 origin 白名单。如果前端用 Nginx 反代也可以在 Nginx 层加但接口层加更可控。4. 权限系统上线后最容易翻车的几个地方4.1 现象超管能进普通角色登录后菜单空白原因菜单接口只查了permission表没有关联role_permission和user_role或者关联条件写错。常见的是user_role里存了 role_id但查询时用了 user_id 去匹配 role 表主键。解决把权限查询封装成一个方法用单元测试覆盖。查询链路是admin_user.id - user_role.role_id - role_permission.permission_id - permission。每一步都打印 SQL 确认不要靠猜。4.2 现象接口返回 403但数据库里明明有权限原因中间件拼的 slug 和permission.path存的不一致。ThinkPHP6 控制器名大小写、多级控制器、路由别名都会影响$request-controller()的返回值。解决在中间件里加日志把拼出来的 slug 和用户权限集合都打出来。更稳妥的做法是直接用路由规则作为权限标识在路由定义时给每个接口绑定permission名称而不是靠控制器名拼。4.3 现象修改角色权限后用户要重新登录才生效原因权限缓存没有失效。用户权限存在 Redis 里角色权限变更时只更新了数据库没删缓存。解决在角色权限保存成功后触发事件删除该角色下所有用户的权限缓存。如果用户量大可以用版本号机制每个角色维护一个perm_version缓存 key 里带上版本号变更时版本号加一旧缓存自然失效。4.4 现象前端路由能访问但接口报 401原因token 过期或前端请求没带 Authorization header。前后端分离项目里前端路由守卫只管页面跳转接口鉴权是另一套。解决前端在 axios 拦截器里统一加 header后端 JWT 中间件校验失败返回 401前端拦截 401 跳登录页。注意 token 刷新逻辑要幂等避免并发请求同时刷新导致旧 token 失效。4.5 现象删除角色后用户还能用旧权限访问原因删除角色时只删了role表记录role_permission和user_role里的关联数据成了孤儿数据查询时仍然能关联出权限。解决删除角色用事务同时清理关联表。或者用软删除查询时过滤status0的角色。数据库外键约束在 ThinkPHP 迁移里默认不开要自己保证一致性。5. 把权限校验做成可复用的注解与命令行工具5.1 用注解替代中间件里的硬编码 slug中间件里拼 slug 的方式在控制器改名后容易失效。进阶做法是用 PHP 8 的 Attributes 或 Doctrine 注解在控制器方法上直接标权限?php namespace app\admin\controller; use app\admin\annotation\Permission; class User { #[Permission(admin/user/list)] public function list() { // 业务逻辑 } }然后在中间件里用反射读取当前方法的注解拿到权限标识再校验。这样权限标识和代码在一起改方法名时不容易漏改。参数说明注解解析可以用roave/better-reflection或自己写简单的正则但生产环境建议用成熟库避免解析异常导致权限绕过。5.2 写一个命令行工具检查权限数据一致性上线前跑一遍权限检查能省掉很多半夜排障php think perm:check这个命令做三件事一是找出role_permission里引用了不存在permission_id的脏数据二是找出user_role里引用了不存在role_id的记录三是列出所有typeapi但没有任何角色关联的权限项提示可能是漏配。实现上用 Db 查询逐项比对输出表格。这个工具我一般放在 CI 里每次迁移后自动跑。5.3 验证权限是否真的生效用不同角色跑一遍接口不要只测超管。建三个测试账号超管、只读角色、无权限角色。用 curl 或 Postman 分别请求同一批接口确认超管全通、只读只能 GET、无权限全部 403。把这三组请求写成一个 bash 脚本每次改权限相关代码后跑一遍。血泪经验是权限系统的 bug 往往不是“进不去”而是“不该进的进去了”正向测试容易过反向测试才是关键。我自己的习惯是任何权限相关的改动先跑反向用例再看正向。权限这东西没有后悔药上线前多花二十分钟比上线后半夜被叫起来强。希望帮到你。本文还有配套的精品资源点击获取