ARTICLE DETAIL

资讯详情

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

ThinkPHP6前后端分离后台框架:RBAC权限与JWT鉴权实践

ThinkPHP6前后端分离后台框架:RBAC权限与JWT鉴权实践 简介这是一套基于ThinkPHP6开发的现代化后台管理框架面向PHP中高级开发者及企业级应用架构学习者解决传统后台系统耦合度高、权限管理粗放、前后端协作低效等痛点。资源共599个文件包含484个核心PHP业务与配置文件、25份Markdown文档说明、18个JSON配置及环境变量文件辅以YML、SQL、CSS、JS等配套资源整体压缩包仅798KB轻量易部署。目前已有83人下载学习适合快速搭建RBAC权限体系、理解前后端分离API设计规范、掌握ThinkPHP6新特性实践。框架已集成角色-用户-权限三级关联模型、标准化RESTful接口、基础CRUD模块及Admin前端入口目录结构清晰含.env示例、PHPUnit测试配置、Git规范文件及多环境部署支持可直接用于教学演示或中小项目快速落地。1. 项目拆解这套ThinkPHP6后台框架到底做了什么1.1 拿到手之后的第一印象一个“非典型”PHP后台看到这个标题的第一眼最值得注意的不是“ThinkPHP6”而是“前后端分离”和“RBAC”这两个词。国内PHP圈子做后台管理绝大多数情况是服务端渲染那一套控制器里render模板模板里用Volist循环输出再用jQuery做点交互。这套框架敢把前后端拆开说明设计思路已经脱离了传统PHP写后台的路径依赖更接近现在主流的前后端分离团队协作模式。再往下看pgj.zip这个包名很直白打包的就是一套完整可运行的源码。这种“一个压缩包交给你解压即用”的项目形态在中小型外包项目、公司内部管理系统、毕设二次开发这些场景里非常普遍。它解决的是“从零搭一套后台太费时间”这个痛点你要做的不是重新发明轮子而是踩在现成轮子上做业务开发。适合看这篇文章的人有两类一类是刚学完ThinkPHP基础语法想看看一个完整后台项目长什么样、怎么组织的初级PHPer另一类是手里有项目要交付需要找一个成熟后台框架做二次开发但又不想上Laravel或者若依那种重型方案的开发者。两类人看完这篇文章都应该能判断这套框架适不适合自己。1.2 为什么选ThinkPHP6来承载“前后端分离RBAC”ThinkPHP6这个版本有个关键变化它彻底转向了“面向API优先”的设计哲学。底层用了依赖注入容器、中间件机制重新设计了请求生命周期同时对路由和PSR-4自动加载要求更严格。这意味着它天然适合做API后端一个请求进来走路由解析、中间件鉴权、控制器处理、返回JSON链路上的每个环节都是可编程可控制的。这里补充一个我在实际对比中的感受。Laravel确实是全球PHP第一框架功能强大生态完整但它的学习曲线陡峭部署要求高软连接、完全限定类名、Opcache缓存策略)在低配服务器上跑起来明显偏重。ThinkPHP6则保留了大量“约定优于配置”的轻量特性一个Nginx就能跑Composer依赖树也比Laravel干净很多。如果你团队里的人是老PHP出身转ThinkPHP6没有任何心理障碍转Laravel就未必了。国内用ThinkPHP还有一个现实理由中文文档完善、社区活跃、大量现成扩展包。做后台管理系统这种业务密集型的项目你需要的不是框架有多炫技而是“我搜一个搜索框怎么实现马上就能搜到答案”。这套框架选择ThinkPHP6做底座从工程交付的角度看是稳妥且可控的决策。1.3 项目目录结构的隐藏信息量解压pgj.zip之后目录结构基本沿用了ThinkPHP6的标准布局但仔细看还是能发现几个差异点wwwroot/ ├─ app/ │ ├─ controller/ // 模块控制器目录 │ ├─ middleware.php // 全局中间件定义 │ ├─ common.php // 公共函数 │ └─ provider.php // 容器绑定配置 ├─ config/ ├─ public/ │ ├─ index.php // 入口文件 │ ├─ router.php // 开发调试路由 │ └─ .htaccess // Apache重写规则 ├─ route/ │ └─ app.php // 路由注册文件 ├─ runtime/ // 缓存、日志 ├─ extend/ // 自定义扩展类 └─ .env // 环境配置一个值得注意的细节很多从ThinkPHP5升级上来的开发者习惯把控制器按模块拆成admin/、home/两个目录但TP6默认是单应用模式控制器全部放在app/controller下面。这套框架如果按标准单应用模式组织那么控制器名就能直接映射到接口路径上比如UserController对应/user/index这种形式。这个设计让路由配置大幅简化API接口的命名天然具备可读性。另外一个容易忽略的东西是.env文件。ThinkPHP6的配置加载逻辑是.env环境变量 动态配置 config目录默认配置。密码、数据库连接串、调试开关这些敏感信息和环境信息分场景配置能让代码在不同机器之间无缝迁移。部署到测试机和线上服务器时你只需修改.env而不需要动任何业务代码这点对前后端分离项目尤其重要——因为你可能同时要面向多套测试环境发布。2. RBAC权限模型整个后台系统的安全地基2.1 RBAC三张核心表和一套扩展表RBACRole-Based Access Control基于角色的访问控制是目前最成熟的通用权限解决方案。它的核心思想可以概括为权限不直接挂在用户身上而是挂在“角色”这个中间层上。用生活里的话说小区门禁系统里你不是直接有“进3号楼2单元的门禁权限”而是你被赋予了“3号楼住户”这个角色这个角色才对应了具体的门禁权限。哪天你搬家了物业把你的角色改成“访客”你这张卡能刷的门自然就变了不需要物业挨个门禁重新录入。这套框架的RBAC模块核心是经典的三表模型-- 用户表记录后台登录账号 CREATE TABLE sys_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT 加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 显示昵称, status tinyint(1) DEFAULT 1 COMMENT 状态1启用 0禁用, last_login_time datetime DEFAULT NULL, last_login_ip varchar(20) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 角色表 CREATE TABLE sys_role ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 角色名称, code varchar(50) DEFAULT NULL COMMENT 角色标识, remark varchar(255) DEFAULT NULL COMMENT 备注, status tinyint(1) DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 权限节点表 CREATE TABLE sys_menu ( id int(11) NOT NULL AUTO_INCREMENT, parent_id int(11) DEFAULT 0 COMMENT 父节点ID, title varchar(50) NOT NULL COMMENT 菜单/节点名称, type tinyint(1) DEFAULT 1 COMMENT 类型1目录 2菜单 3按钮, path varchar(200) DEFAULT NULL COMMENT 前端路由地址或后端接口标识, permission varchar(100) DEFAULT NULL COMMENT 权限标识, sort int(11) DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;除了这三张核心表再加上两张关联表-- 用户-角色关联表 CREATE TABLE sys_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 sys_role_menu ( role_id int(11) NOT NULL, menu_id int(11) NOT NULL, PRIMARY KEY (role_id, menu_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意这五张表的字段设计里暗藏了一个细节用户名在数据库层面就加了唯一索引。这个小细节在实际项目中非常重要它保证了你即使用户并发点了两次注册或添加数据库层面也会帮你拦截重复数据而不只是靠应用层校验。很多初学者做后台系统用户可重复添加最后权限数据错乱问题就出在这里。2.2 权限判定流程一次请求是怎么被拦截的理解RBAC的权限判定流程是看懂这套框架后端代码的关键。我画了一条完整的请求链路用文字描述不依赖图表工具第一步浏览器发起一个请求到/admin/user/add前端先检查本地存储的用户Token是否过期过期就先跳转登录页没过期则带着Token发请求。第二步请求到达Nginx按规则Rewrite到public/index.php入口。第三步ThinkPHP6内核启动路由解析出对应的控制器和方法。第四步框架执行全局中间件队列其中一个核心中间件专门做JWT Token校验。它从Header的Authorization字段取出Token验签、确认合法后解析出用户ID存入请求上下文。第五步控制器执行前权限中间件拿到当前请求的控制器名和方法名拼接成user/add这个权限节点标识去sys_role_menusys_menu这两张表里查当前用户的角色是否有这个节点的权限。第六步如果查询结果里有匹配记录放行没有匹配记录返回统一的JSON格式无权限提示HTTP状态码通常是403。这里最容易被初学者忽略的是RBAC判定到底是判定路由还是判定菜单答案是要先判定菜单。也就是说你必须在sys_menu表里把“新增用户”这个操作注册成一个按钮类型的节点给某个角色勾上这个按钮这个角色下的用户才能新增用户。如果节点没注册或者角色没勾选即使你手动输入URL想直接调用后端接口也会被权限中间件拦截。这种设计有一个很明显的安全价值权限控制真正做到了后端而不是前端把按钮隐藏了就万事大吉。前端只负责“看不看得到入口”后端负责“到底能不能操作”即使是前端工程师临时改代码把隐藏按钮调出来后台中间件仍然会把请求挡在门外。这套后台框架把这个链路写得清晰规范二次开发时可以原样复用。2.3 角色冲突和超级管理员处理项目里会出现一个真实场景一个用户被赋予了多个角色比如“运营专员”和“活动管理员”两个角色。肯定会出现一个角色能编辑文章、另一个角色不能的情况。这时的权限判定策略是什么系统里一般有两种做法一是并集策略只要任意一个角色有权限就放行二是交集策略所有角色都有权限才放行。后台管理系统实际项目里90%以上采用的是并集策略。原因很简单你给一个人多分配一个角色本意就是给他加权限而不是减权限。如果采用交集策略角色的管理维护会非常痛苦你每设计一个新角色都要考虑和已有角色做横向比较权限会越收越死维护成本极高。超级管理员在这个框架里通常有两种处理方案。一种是用固定的用户ID比如ID1来硬编码判断代码里到处写if ($userId 1)这种方式简单但埋了雷——万一ID1用户被删了或者改名了判断就失效了。另一种更优雅的做法是加一个is_super字段比如sys_user表加一列is_super tinyint(1) DEFAULT 0超级管理员的该值为1权限中间件里优先检查这个字段如果是超级管理员直接放行根本不走查询权限表那一套逻辑。这个方案在高并发场景下有细微的性能优势但更大的意义在于代码语义清晰你看到is_super字段就知道这个人是超级管理员而不是靠某个约定俗成的ID数字。我做权限系统时有一个习惯——任何权限相关判断都必须有显式的字段或标识绝不写魔法数字。这一条经验建议你在二次开发时也遵守。3. 前后端分离的设计精髓从登录到部署的完整链路3.1 JWT鉴权为什么不用Session传统ThinkPHP写后台登录成功后服务端创建Session返回一个Cookie浏览器下次请求带上Cookie服务端根据Session ID找到登录状态。前后端分离之后前端页面可能部署在admin.example.com后端接口部署在api.example.com浏览器跨域请求Cookie默认不携带需要额外配CORS的withCredentials而且一旦水平扩展多台服务器Session默认存在单机内存里会出现“用户在这台服务器登录了请求被负载均衡到另一台服务器就变成未登录”的经典问题。这套框架选择JWTJSON Web Token方案来解决这些问题。JWT的原理可以打个比方用户登录成功后服务端不保存任何状态只发给你一张盖了数字签名的“通行证”。这张通行证上写着“用户ID、过期时间、签名信息”。之后你每次请求把它放在Header里带给服务端服务端验一下签名是否有效、是否过期就能确认“你是你”不需要查数据库、不需要共享存储。JWT的实际落地在这套框架里是这样实现的use Firebase\JWT\JWT; public function login() { $username $this-request-post(username); $password $this-request-post(password); // 验证用户 $user UserModel::where(username, $username)-find(); if (!$user || !password_verify($password, $user-password)) { return json([code 4001, msg 用户名或密码错误]); } // 构造Payload $payload [ uid $user-id, iat time(), // 签发时间 exp time() 7200 // 2小时过期 ]; $token JWT::encode($payload, env(JWT_SECRET), HS256); return json([ code 200, data [ token $token, expires_in 7200 ] ]); }这里要重点提一个安全细节登录成功后这套框架在写入密码时使用了password_hash()函数校验时用password_verify()。这两个函数是PHP官方推荐的密码哈希方案自动加盐每次哈希结果都不同暴库之后你拿到的只是随机字符串无法直接还原成明文密码。如果你看到老项目里用MD5加盐甚至裸MD5存密码可以负责任地说——那个项目存在严重安全隐患尽早改掉。3.2 跨域问题的完整解决方案前后端分离项目中跨域是新手开发者遇到的第一座山。之所以有跨域限制是浏览器出于安全考虑禁止一个源下的文档或脚本去请求另一个源的资源。这套框架的API跑在api.example.com前端管理后台跑在admin.example.com端口也可能各不相同比如本地开发前端跑8080后端跑8000这就算跨域浏览器默认拦截。解决跨域常见有三个思路Nginx反向代理、后端CORS头、前端JSONP。这套框架走的是主流的后端CORS方案核心是在响应头里加上Access-Control-Allow-Origin: http://admin.example.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Authorization, Content-TypeThinkPHP6里实现这个功能最优雅的方式是注册一个全局中间件在中间件的handle方法里给所有响应追加这些Headernamespace app\middleware; use Closure; use think\Request; use think\Response; class CorsMiddleware { public function handle($request, Closure $next) { $origin $request-header(origin, ); // 允许的域名白名单生产环境务必配置真实域名 $allowOrigins [ http://admin.example.com, https://admin.example.com, ]; $header [ Access-Control-Allow-Origin in_array($origin, $allowOrigins) ? $origin : , Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS, Access-Control-Allow-Headers Authorization, Content-Type, X-Requested-With, Access-Control-Max-Age 86400, ]; // 预检请求直接返回200 if ($request-method() OPTIONS) { return Response::create(, json, 204)-header($header); } $response $next($request); $response-header($header); return $response; } }关于这个中间件两个容易踩坑的细节必须提醒。第一OPTIONS预检请求必须处理。浏览器在发送真正的POST请求之前发现请求带自定义Header比如Authorization会先发一个OPTIONS请求来试探服务器允不允许如果服务器不响应这个探路请求浏览器会直接拦截真正的请求。很多新手在前后端联调时发现请求一直失败Network面板里红色报CORS错误查了半天才发现是预检请求没过。解决方式就是上面代码里明显写出的OPTIONS直接返回204。第二Access-Control-Allow-Origin不要直接配置成*。如果你设置成*就意味着任何网站都能通过浏览器直接调用你的接口。这对于一个带Token鉴权的API系统来说虽然Token本身拦截了未授权访问但暴露了API可以被任意第三方站点调用的风险面。正确的做法是维护一个域名白名单数组只有白名单里的域名才返回对应的CORS头。上面中间件里我已经把这种写法放进去了实际开发时填上你真实的前端域名就行。3.3 接口返回格式和错误码约定前后端分离项目里前后端工程师之间唯一的契约就是接口文档和返回格式。这套框架统一了JSON返回结构这是它做得好用、前后端联调顺畅的一个关键原因。// 成功返回 { code: 200, msg: success, data: { list: [], total: 100 } } // 失败返回 { code: 4002, msg: 登录状态已过期请重新登录, data: null }这种统一结构优点是前端可以直接封装一个request.js拦截器只要响应里的code不是200直接弹Toast提醒。不需要每个接口都单独判断数据结构业务代码就会特别干净。框架的错误码约定可以按照模块和业务来分大类错误码含义场景200成功一切正常4001参数错误必填字段缺失、格式不对4002认证失败密码错误、Token过期4003无权限RBAC拦截当前账号无节点权限5000服务器内部错误代码异常、数据库异常这里讲一个我在实际项目里总结的经验错误码的约定一定要从第一天就定死并写进接口文档前后端都严格遵守。最忌讳的是同一个业务场景今天返回4001、明天返回4002前端写着写着就要写一大串if else区分各种错误码维护成本直线上升。好的错误码约定是“前端看到code就能判断要不要跳登录页、要不要刷新列表、要不要弹确认框”这样联调就能省大量时间。4. 核心实战环节从零部署到跑通业务4.1 本地环境搭建和项目初始化拿到pgj.zip之后第一步自然是解压到本地Web环境。这里我用的是最经典的PHPStudy/Wnmp套件组合PHP版本必须≥7.4ThinkPHP6官方要求如此另外需要开启fileinfo、pdo_mysql、curl这几个扩展。注意PHP 8.0兼容TP6有已知兼容性问题不同框架版本对PHP 8.x的支持程度不一样建议稳妥起见容器或者套件里多备一个PHP 7.4版本。接下来是安装依赖。项目解压之后第一步不是配数据库而是先跑Composer命令cd wwwroot composer install这个命令会从Packagist拉取项目依赖的所有第三方库JWT、验证码、导出组件等生成vendor目录。如果你本地没安装Composer需要先去官网下载安装或者Windows用户下载Composer-Setup.exe直接装。然后创建.env文件项目里通常会有.env.example模板直接复制改名即可配置好数据库连接APP_DEBUG true [DATABASE] TYPE mysql HOSTNAME 127.0.0.1 DATABASE pgj_admin USERNAME root PASSWORD 123456 HOSTPORT 3306 CHARSET utf8mb4 [JWT] SECRET your-secret-key EXPIRE 7200这里我要强调一个细节APP_DEBUG在生产环境一定设为false。开启调试模式时ThinkPHP会把数据库查询日志、异常堆栈、编译缓存这些信息都打印出来这对开发期排错很友好但生产环境暴露给用户就是严重的信息泄漏隐患。操作逻辑是开发机打开生产机关闭这个开关切换要像切电源一样果断。接着把项目根目录下数据库文件一般在sql文件夹或者database目录或者README里有说明导入MySQL。这个框架的SQL文件里不仅包含了表结构还会默认插入一个超级管理员账号常见的如admin/admin123导入后第一件事就是赶紧去改密码防止别人用默认口令登进后台。4.2 Nginx/Apache的Rewrite配置前后端分离项目部署最容易出问题的是Web服务器配置。前端项目打包后是一堆静态文件扔到任何静态托管空间都能跑后端接口则是动态请求必须Rewrite到ThinkPHP的入口文件。以Nginx为例一个完整的后端站点配置server { listen 80; server_name api.example.com; root /var/www/pgj_admin/public; index index.php; # 关键ThinkPHP 6 的URL重写规则 location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; break; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~ /\.(?!well-known).* { deny all; } }这套配置有几个关键点root指向项目的public目录而非项目根目录这是ThinkPHP官方和该框架都统一推荐的部署结构。public目录里只有入口文件index.php以及静态资源CSS/JS/图片用户看到的目录结构不会暴露应用代码。如果有人试图访问/app/controller/UserController.php这种路径因为Web根目录已经指向public他会得到404而不是看到你的源代码。另外需要注意Apache环境下的.htaccess重写规则。项目public目录下自带了一个.htaccess文件如果你用Apache确保开启了mod_rewrite模块并在虚拟主机配置里加一行AllowOverride All否则路由重写不生效你会看到所有接口都返回404。4.3 跑通第一个接口登录-获取用户信息部署完成后我先用接口调试工具Postman/Apifox来验证整个流程。这个验证步骤很小但价值极大它能帮你确认“环境配置没问题、数据库连接正常、路由和权限链路是通的”是一个最小化的端到端测试。登录接口在框架里固定为POST /api/login Content-Type: application/json { username: admin, password: admin123 }成功响应{ code: 200, msg: success, data: { token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..., expires_in: 7200 } }拿到Token之后调用获取用户信息接口来验证鉴权中间件是否生效GET /api/user/info Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...如果中间件配置正常这个请求会返回当前用户基本信息以及他拥有的权限节点列表前端根据这个列表渲染菜单和按钮。如果Token缺失或过期响应是{ code: 4002, msg: Token已过期或无效, data: null }走到这一步说明前后端分离的鉴权闭环已经打通。接下来就可以把前端工程跑起来配置好Vite或Webpack的代理把/api开头的请求转发到后端地址开始愉快的业务开发。4.4 用代码生成器加速业务模块开发这套框架里我比较喜欢的一个模块是代码生成器。后台管理系统的业务开发套路极其固定建表-写模型-写控制器-写视图/接口-写权限节点。这套流程重复度太高纯手写既浪费时间又容易出错代码生成器就是后台框架的“工业流水线”。在框架里代码生成器可能以命令行的形式存在比如php think gen:crud或者在后台管理界面的一个功能模块里。核心逻辑是你告诉它数据表名sys_notice它自动帮你生成模型文件app/model/Notice.php控制器文件app/controller/NoticeController.php包含增删改查四个标准方法验证器文件app/validate/NoticeValidate.php生成前端列表增删改查对应接口并自动在权限菜单表里插入这些节点记录// 一个由生成器产出的标准控制器 namespace app\controller; use app\BaseController; use app\model\Notice; use think\Request; class NoticeController extends BaseController { protected $model; public function __construct() { $this-model new Notice(); } // 列表 public function index() { $page $this-request-param(page, 1); $limit $this-request-param(limit, 10); $list $this-model -where(status, 1) -order(id, desc) -page($page, $limit) -select(); $total $this-model -where(status, 1) -count(); return json([ code 200, data [ list $list, total $total ] ]); } // 新增 public function save() { $data $this-request-post(); $this-model-save($data); return json([code 200, msg 添加成功]); } }生成的代码虽然不是最精简的但胜在结构规范、风格统一二次修改成本极低。直接开始写业务逻辑比从空文件敲起来省了大概一个下午的时间。有一点要提醒生成器生成的代码只是“能用”的标准件涉及特殊业务逻辑的地方比如通知要群发、要定时发布还是得手工改造不能指望生成器全自动解决一切需求。5. 常见问题与排查技巧实录5.1 接口返回404路由没配还是Nginx没重写这是后台接口联调时遇到频率最高的问题。一旦前端请求/api/user/xxx返回404排查路径按顺序是第一步先确定是不是Nginx重写出了问题。直接在浏览器访问http://api.example.com/index.php/api/user/info如果加了index.php能访问、去掉了404那九成是Nginx的rewrite规则没生效回看4.2节的配置重点检查有没有location /这个大括号里的if和rewrite。第二步检查ThinkPHP6的路由文件。打开route/app.php确认你请求的路径是否真的注册了路由。路径区分情况如果框架是典型的RESTful风格路由通常会写清楚如果你是直接用控制器/方法这种默认方式访问就不需要路由注册直接靠MVC约定。第三步查看ThinkPHP的调试页。开启APP_DEBUG后访问错误地址页面会显示完整的异常信息包括哪个控制器不存在、哪个方法不存在。根据提示修改控制器名或方法名即可。这三个步骤按顺序排查95%的404问题能在五分钟内解决。5.2 前端报CORS错误多半是预检请求没过关前端联调时常见的报错是Access to XMLHttpRequest at http://api.example.com/api/login from origin http://localhost:8080 has been blocked by CORS policy。看到这个报错不要慌先看两点第一点后端中间件有没有加OPTIONS方法的直接响应。绝大多数情况下后端只处理了POST/GET预检的OPTIONS请求落到控制器里报405或者404浏览器拿不到预期的响应头就拦截了。回到3.2节的CorsMiddleware确保中间件已经单独处理了OPTIONS请求。第二点检查请求头发送的是不是Authorization这个自定义Header。CORS允许的请求头在白名单里如果前端用的是Authentication或者X-Token这种非标准HEADER名后端必须把Access-Control-Allow-Headers增加到对应的值。比较典型的遇到场景是前端从老项目里拷贝了封装代码Header名没统一后端报错了你都不知道去哪里找。5.3 验证码不刷新或者不显示后台登录搞验证码是这个框架的标准做法。验证码不显示有几个常见原因一是ThinkPHP的验证码组件需要额外安装topthink/think-captcha扩展包如果Composer install时没装全就会直接报“类不存在”错误二是验证码输出的是图片流接口返回必须是image/png格式你不能把它包在JSON里返回三是验证码Session存储依赖服务端Session配置别把Session自动关闭了。验证码刷新机制的一个小技巧前端拿到新的图片验证码时要让浏览器设置一个不同的请求参数时间戳防止图片被浏览器缓存。这一点在前后端分离下比传统Session场景更容易踩坑因为Token是局部刷新的但验证码是独立的一张图片浏览器缓存策略会直接让验证码图片“看起来永远不变”。5.4 登录成功但接口依然401登录成功了返回的Token都拿到手了但后面调用所有接口都返回401。这个问题的根源几乎都在前端请求封装上。排查方法是打开浏览器开发者工具Network面板点一下出错的请求查看Request Headers里有没有Authorization这个Header。常见情况有三种第一种是不带Header直接请求请求头发送里压根没有Authorization这个是前端拦截器没写好。第二种是Header名拼错了比如后端要求的是Authorization你写成了authorization大小写有区别HTTP头虽说不区分大小写但某些网关层会做区分保险起见规范写法导致后端取不到值。第三种是Token拼接格式不对后端要的是Bearer eyJhbGciOi...前面带一个空格分隔Bearer前缀和Token串有些初学前端只把Token裸值放进去后端解析时自然失败。碰到这类问题建议前后端一起看一份调试文档明确约定Header取什么名字、Token带不带Bearer前缀、过期后返回什么错误码。没有这个约定前后端联调时会出现各写各的情况出了问题互相甩锅的情况时有发生。5.5 深度体验几个值得关注的细节日志系统优先记录到runtime目录遇到接口异常建议先看runtime/log/下的日志文件里面会记录完整的请求参数、SQL语句、异常堆栈。这个信息量比前端只看一个500错误码丰富太多。文件上传功能如果做头像或附件要注意public/storage目录的写权限。PHP-FPM进程通常以www用户运行如果该目录属于root导致不可写上传就会报“调用upload接口返回500”。解决方案是chown -R www:www storage/这个权限问题在Linux服务器上属于高频坑。计划任务定时任务在后台框架里也不可缺席。比如一些过期数据清理、定时统计可以写一个自定义命令行php think cron:notice然后在系统crontab里配置*/5 * * * * cd /var/www/pgj_admin php think cron:notice runtime/log/cron.log 21这样一个自动扫描消息并推送的用户通知任务就定时跑起来了。6. 安全加固与生产环境部署经验6.1 代码级安全SQL注入和XSS防护ThinkPHP6的ORM查询默认使用PDO预处理能有效防住SQL注入前提是你得用对方法。这里有一个使用禁忌项目里最常见的注入风险来自直接拼接SQL字符串的写法比如// 反面教材直接拼接变量存在SQL注入风险 $result Db::query(SELECT * FROM user WHERE username{$username});一定不要这样写。正确方式是使用框架的查询构造器或者参数绑定// 正确写法查询构造器会自动做参数绑定 $result Db::table(user)-where(username, $username)-find();XSS防护方面前后端分离项目的重点在“后端输出转义”。凡是后端返回给前端的变量中可能包含用户输入的内容比如公告标题、用户昵称都不能假定前端做好转义更不能保证接口被第三方调用时无人作恶。最佳实践是后端统一对字符串做htmlspecialchars()过滤后再输出或者前端在Vue渲染时使用插值表达式默认转义HTML避免直接使用v-html渲染不可信内容。6.2 服务器层面的安全基线部署到生产环境前几个基础安全项必须做掉一是修改默认管理员密码。不要用admin/admin123这种组合密码必须10位以上包含大小写字母、数字、特殊字符。这里有个实用技巧可以用浏览器密码管理器或Keepass生成随机密码不要自己编一个“看起来好记”的密码。二是关闭调试模式。.env里把APP_DEBUG改成false首页就不会再展示版本号和异常堆栈。三是MYSQL数据库权限最小化。给应用分配一个专用账号不直接用root连接。比如CREATE USER pgj_applocalhost IDENTIFIED BY 复杂密码; GRANT SELECT, INSERT, UPDATE, DELETE ON pgj_admin.* TO pgj_applocalhost; FLUSH PRIVILEGES;“最小权限原则”意味着即使应用被攻击攻击者也无法执行DROP DATABASE或读取其他库的数据能把损失限制在最小范围。6.3 前端部署和反向代理的配合前端工程通常是Vue/React打包之后是纯静态文件部署到Nginx时可以跟前端页面非同一域名实现动静分离。生产环境推荐架构是一个Nginx监听80/443按路径转发。/api开头的流量走后端PHP-FPM/开头的流量走前端静态文件。在同一域名下部署还能规避掉一部分CORS开发期问题——生产环境同源访问浏览器根本不会触发跨域拦截性能和安全都更优。server { listen 80; server_name admin.example.com; # 前端静态文件 location / { root /var/www/pgj_admin_web/dist; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这套方案的好处是对外只暴露一个入口admin.example.com、一个80/443端口链路简单、排查问题方便浏览器也不存在跨域对前端配置要求大幅降低。很多初学项目直接打包静态文件扔到七牛云OOS上还要单独维护API域名反而把部署复杂度推高了。我个人的建议是项目初期如果上不了HTTPS、没有专业运维支持就用这种“同一域名动静分离”的部署方式先把业务跑起来等团队和项目成熟了再做微服务化拆分也不迟。7. 二次开发实战如何往这个框架里加一个业务模块7.1 明确需求做一个简单的公告管理用这个后台框架实际做一个公告管理模块可以完整体验从设计到上线的全流程。需求很典型管理员可以发布公告、修改公告、删除公告普通用户只能查看已发布公告列表和详情。这个模块既有典型的CRUD基础环节又有RBAC权限控制的业务差异非常适合演示二次开发的完整链路。数据表设计其实很直白CREATE TABLE sys_notice ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 标题, content text COMMENT 内容, status tinyint(1) DEFAULT 1 COMMENT 1发布 0草稿, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建议把create_time和update_time字段一定加上这两字段在后台列表页、数据统计、审计追踪里几乎是必用的。ThinkPHP6如果模型里定义了autoWriteTimestamp写入数据时会自动帮你填上时间戳不用在控制器里手动赋值。7.2 后端开发模型、控制器、路由三步走模型层是ThinkPHP6里相对较重的一层可以直接把数据表字段转成属性复用ORM的关系关联、时间戳自动写入、软删除等功能。当然也可以用这个模型实现更复杂的业务逻辑比如指定类型查询、公告数据统计namespace app\model; use think\Model; class Notice extends Model { // 设置时间戳字段 protected $autoWriteTimestamp true; // 自动只查已发布状态 public function scopePublished($query) { return $query-where(status, 1); } public function getStatusTextAttr($value, $data) { return $data[status] 1 ? 已发布 : 草稿; } }控制器层的核心逻辑是做参数校验防止非法数据入库public function save() { $data $this-request-post(); $validate new Validate([ title require|max:100, content require ]); if (!$validate-check($data)) { return json([code 4001, msg $validate-getError()]); } $this-model-save($data); return json([code 200, msg 保存成功]); }ThinkPHP6里定义消息是用$this-request-post()这种便捷方法但你也可以用Request门面全局获取参数。无论哪种方式核心原则是控制器只做参数接收和流程控制业务逻辑尽量下沉到模型层或者独立的服务类这样代码可读性和维护性更好。路由注册在route/app.php里Route::group(api, function () { Route::get(notice/list, Notice/index); Route::get(notice/:id, Notice/read); Route::post(notice/save, Notice/save); Route::put(notice/update/:id, Notice/update); Route::delete(notice/delete/:id, Notice/delete); })-middleware([CheckToken::class, CheckPermission::class]);注意这里路由分组的中间件配置CheckToken负责认证CheckPermission负责授权两者的区别前面已经系统分析过。一个非常重要但也容易遗漏的细节是/notice/list和/notice/:id这两条是公开的任何人都能看公告但它们仍然要走Token认证——即使普通用户登录后也能看鉴权只要保证“是合法登录用户”即可不需要校验特定权限节点。7.3 前端页面对接Vue/React / 或传统模板如果你在二次开发时决定保留传统模板渲染服务端渲染的一种变种在控制器里也可以直接返回视图渲染结果不需要强行前后端分离。比如展示公告详情页面直接public function page($id) { $notice Notice::find($id); return view(notice/detail, [notice $notice]); }但既然这套框架主打前后端分离更标准的做法是前端用Vue 3 Element Plus做一个列表页配置路由层面动态生成。前端列表页在mounted生命周期里请求后端接口拿数据渲染表格增删改查操作对应的框架事件方法内调用后端API。这种模式的日常操作逻辑特别统一进入页面onMounted里调用getList()。点击新增按钮弹窗里填表单确认后调save接口。表格行内点编辑把当前行数据回填到弹窗表单确认后调update接口。点击删除弹确认框然后调delete接口。另外如果使用如Vue的动态路由方案前端渲染菜单依赖权限节点数据。登录成功后后端返回当前用户的permissions数组前端拿这个数组过滤路由表就能实现“不同角色登录后看到不同菜单和不同页面”的效果。这个机制要和后端RBAC严格对应前端控制可见性后端控制可操作性两者结合才是完整方案。7.4 测试和上线两个容易忽略的步骤模块开发完成后上线前别急着打包花两分钟检查两个东西。第一是接口权限节点有没有录入sys_menu表。如果没录入Super管理员账号当然可以直接用但普通角色就算分配了角色也访问不了这个模块的接口。这个bug属于隐蔽型开发阶段用Super管理员测试一切正常一换账号就傻眼很容易让人误以为是前端路由的问题实际上后端权限没开。第二是让后端工程师或产品经理走一遍完整的业务测试用例。公告管理的用例至少包括新建、编辑、发布、删除、权限控制非管理员看不到管理入口、状态切换发布/草稿、数据分页、搜索关键词。把这条主干流程跑通再上线不迟。8. 最后的实操心得8.1 踩过几次坑之后的一些总结前后端分离的ThinkPHP6后台框架归根到底解决了三件事一是后端接口化让前端不再被PHP模板绑死二是权限模型标准化让系统的安全控制有章可循三是开发流程规范化让多人协作的接口对接和部署不再是一场灾难。但在实际使用中我最后想分享的经验是框架是别人的思路业务才是你自己的。这套pgj.zip给你的是一个稳定的骨架和一套可复制的最佳实践但它的权限表设计、中间件逻辑、布局风格都只代表开发者本人的取舍。真正的成长不是把别人的框架原封不动拿来用而是搞清楚它每个设计背后的为什么然后根据自己的业务做调整打磨。比如你会发现在高并发场景下JWT的过期时间设计能不能做到“续签”而不是“重新登录”比如你的权限体系里是否还需要“数据权限”层级不是判断能不能操作而是能操作哪些数据范围再比如你的后端接口是否需要增加幂等处理或消息队列这些问题没有标准答案但利用好这套框架的底子你有足够好的起跑位置去探索自己的答案。如果这篇文章对你有帮助实操中遇到具体问题欢迎在评论区把你的报错信息、请求截图和排查过程发出来经验就是靠这样反复碰撞积累出来的。本文还有配套的精品资源点击获取
返回列表