在ThinkPHP 6.0中,必须通过中间件统一设置响应头,直接调用header()或控制器内response()->header()均失效;需在中间件handle()中对$next($request)返回的$response链式调用withHeader()并return,且跨域中间件须优先注册、拦截OPTIONS请求并动态设置Access-Control-Allow-Origin。

在 ThinkPHP 6.0 中通过中间件统一修改响应头,是避免控制器重复写 header()、防止头被框架覆盖、确保安全策略生效的唯一可靠路径。直接调用 PHP 原生 header() 会失效,response()->header() 在控制器里写两行(先构造再设头)也会被丢弃,只有在中间件中对 $response 实例链式注入并 return 才稳定生效。
新建全局安全响应头中间件
在 app/middleware/ 目录下创建 SecurityHeaders.php 文件,类名必须与文件名一致,命名空间为 app\middleware;该文件需实现 Handle 接口,handle 方法接收 $request 和 $next 闭包。
在 handle() 方法内,调用 $next($request) 获取原始响应对象,再对其链式调用 withHeader() 设置头信息——【必须在 return $response 前完成所有 withHeader() 调用】,否则头不会写入最终响应。
返回语句必须是完整的链式表达式或单个 $response 变量,不能拆成多行赋值后再 return,否则中间件流程中断导致响应空内容。
立即学习“PHP免费学习笔记(深入)”;
跨域中间件中正确处理 OPTIONS 预检请求
浏览器发起带 Authorization 或 application/json 的请求前,会先发一个 OPTIONS 请求。若中间件未拦截,ThinkPHP 默认返回 405 或静默失败,前端卡在预检阶段。
第一步:在中间件 handle() 开头判断是否为 OPTIONS 请求:if ($request->isOptions()) { return response('', 204); }
第二步:确认该中间件在 app/middleware.php 中注册于数组首位,否则日志、JWT 等前置中间件可能已触发输出,导致 headers already sent 错误。
第三步:在 $next($request) 后设置 Access-Control-Allow-Origin 等头,但注意——【若前端启用了 credentials,Access-Control-Allow-Origin 不得为 *,必须动态匹配白名单域名】,否则浏览器直接拒绝响应,连调试面板都看不到后续请求。
控制器中临时覆盖响应头的两种方式
方法一:使用 json() 快捷方法传 headers 参数
直接在控制器 return 语句中写:return json(['msg' => 'ok'], 200, ['X-Trace-ID' => uniqid()]); 这种方式只影响当前接口,不污染全局,适合埋点或调试标识。
方法二:显式构造 Response 对象并链式设头return response(['data' => $list])->header('Cache-Control', 'no-cache')->code(200); 注意:code() 必须放在最后或紧跟 header() 后,否则状态码可能被后续操作覆盖。
⚠️ 提醒:这两种方式都仅对当前控制器方法生效,且 headers 参数必须是关联数组,键名区分大小写,值不能为空字符串,否则部分 Nginx 版本会过滤掉整个头字段。
避免 CDN 或反向代理覆盖响应头
Nginx 默认会过滤 X-XSS-Protection,Cloudflare 免费版强制重写 X-Frame-Options 为 SAMEORIGIN 并禁止覆盖,阿里云全站加速可能删除自定义头。
验证是否生效:部署后用 curl -I https://your-domain.com/api/test 查看原始响应头,绕过浏览器缓存和代理;若看到的头与中间件设置不一致,问题大概率出在网关层而非 PHP 代码。
解决路径:Nginx 需添加 proxy_pass_header X-XSS-Protection;;Cloudflare 需升级到 Pro 套餐才能关闭自动头注入;阿里云全站加速需在控制台「HTTP 头配置」中手动开启透传。



















