Laravel中应在中间件handle()末尾用$response->headers->set()设安全头,避免被覆盖;CSP需动态生成并按路由/角色差异化配置;禁用X-Frame-Options,仅用frame-ancestors;中间件须注册在web组末尾确保生效。

中间件里直接改 response 头最稳
Laravel 的中间件是加安全响应头的唯一合理位置,别在控制器或服务提供者里硬塞。因为只有中间件能确保每次 HTTP 响应都经过处理,且不会被后续逻辑覆盖。
常见错误是用 header() 函数或在控制器里调 response()->header(),结果在重定向、JSON 响应或异常页面里失效——那些路径根本没走你的控制器。
- 必须在中间件的
handle()方法末尾操作$response对象,不是$request - 用
$response->headers->set(),不是header()(后者会绕过 Symfony 响应生命周期) - 如果用了多个中间件(比如你自定义的 + Laravel 自带的
TrustProxies),顺序很重要:安全头中间件建议放在靠后位置,避免被其他中间件覆盖
Content-Security-Policy 不能写死,得动态生成
CSP 不是贴个固定字符串就完事。一旦用了内联脚本、eval()、或第三方资源(比如 Google Fonts、CDN JS),硬编码的 Content-Security-Policy 会直接让页面白屏或功能异常。
真实场景里,你需要根据环境、用户角色甚至当前路由动态拼接策略。比如开发时允许 'unsafe-inline',生产环境必须禁掉;后台管理页可能需要额外允许某段内联样式,而前台不允许。
- 别用
$response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'self'")这种静态写法 - 推荐封装一个辅助方法,比如
cspPolicyForRoute($request),按$request->route()->getName()或$request->user()?->role返回不同策略 - 注意单引号必须保留,双引号包不住策略值;
"script-src 'self' https://cdn.example.com"是合法的,但漏掉单引号就会被浏览器忽略
X-Frame-Options 和 frame-ancestors 别同时设
这两个头干同一件事:防点击劫持。但 X-Frame-Options 是老协议(IE 8+ 支持),Content-Security-Policy 里的 frame-ancestors 是新标准(Chrome 40+、Firefox 45+)。现代浏览器优先认 frame-ancestors,并忽略 X-Frame-Options —— 除非你故意留着它兼容老 IE。
问题在于:如果你两个都设,某些代理或 CDN(比如 Cloudflare)可能因头重复报错或截断响应;更糟的是,部分旧版 Nginx + FastCGI 组合会把重复头合并成非法格式,导致整个响应头崩溃。
- 只用
frame-ancestors 'none'(CSP 里),就别再设X-Frame-Options - 真要兼容 IE8–10,只设
X-Frame-Options: DENY,且确保 CSP 中不出现frame-ancestors - 检查最终响应:用
curl -I your-site.com看是否出现两个 frame 相关头
中间件注册顺序影响 Content-Security-Policy 生效
Laravel 默认中间件栈里,EncryptCookies、AddQueuedCookiesToResponse 都在响应生成后期执行。如果你的安全中间件注册太早(比如放在 CheckForMaintenanceMode 后面),它设置的 CSP 可能被后续中间件悄悄覆盖——尤其是 Laravel 10+ 引入的 PreventRequestsDuringMaintenance,会在维护模式下替换整个响应对象。
最稳妥的位置是:在所有“可能替换响应对象”的中间件之后,但在 SendRequestToRouter(即路由分发)之前。实际操作就是把你的中间件加到 $middlewareGroups['web'] 数组末尾。
- 在
app/Http/Kernel.php的$middlewareGroups['web']里,把你自定义中间件放到最后几项 - 别用
push()动态注册,容易被服务提供者加载顺序打乱 - 验证方式:在中间件里临时加
Log::debug('CSP set', ['headers' => $response->headers->all()]);,看日志里头是否还在
真正麻烦的是 CSP 里那些隐式依赖:比如 Vue 单文件组件的内联样式、Livewire 的内联脚本、甚至 Tailwind JIT 的 style 标签——它们不会报错,但会让 CSP 策略形同虚设。上线前必须用 Chrome DevTools 的 Console 和 Security 标签页盯着看 violation 报告。


















