唯一可靠路径是http响应头,thinkphp需通过全局中间件统一注入x-frame-options头,禁用nginx/apache的add_header配置以避免冲突,并确保与csp frame-ancestors语义一致。

ThinkPHP 没有内置的点击劫持防护机制,X-Frame-Options 必须由你手动注入响应头,且只能在 PHP 层统一控制——Nginx 或 Apache 的全局 add_header 配置会与框架冲突,导致部分页面漏配、重复设置或静态资源误锁。
为什么不能靠模板或 JS 检测来防御
前端 <meta http-equiv="X-Frame-Options"> 完全无效,所有主流浏览器均忽略;JS Frame-Buster 脚本(如 if (top !== self) top.location = self.location;)依赖执行环境,可被禁用、绕过或延迟触发,攻击者只需在 iframe 加载完成前拦截 JS 即可失效。唯一可靠路径是 HTTP 响应头。
ThinkPHP 6/7 中必须用中间件统一注入
ThinkPHP 不提供开箱即用的 XFO 中间件,也不能在控制器里零散调用 header()(易遗漏、难维护)。正确做法是在全局中间件中注入,确保所有响应(包括 JSON 接口、错误页、下载流)都带上该头:
- 创建中间件:
php think make:middleware SecurityHeaders - 在
handle()方法开头添加:$response = $response->header('X-Frame-Options', 'DENY'); - 注册到
app/middleware.php的全局数组首位,避免被后续中间件覆盖 - 若需按路径差异化配置(如后台用
DENY,嵌入式组件页用SAMEORIGIN),应在中间件内用$request->url()判断路由,而非硬编码
必须关掉 Nginx/Apache 的 add_header 并验证是否唯一
phpEnv、宝塔等环境常在 Nginx 配置里写 add_header X-Frame-Options "DENY";,这会导致两个问题:一是子 location(如 /api/)未显式重写时该头丢失;二是与 PHP 层 header 冲突,响应中出现两次 X-Frame-Options,Chrome 取第一个、Firefox 可能忽略,行为不可控。
操作步骤:
- 检查 Nginx 配置(
nginx.conf或站点 conf),删掉所有add_header X-Frame-Options行,尤其注意http、server和location ~ \.php$块 - 重启 Nginx:
sudo nginx -s reload - 用浏览器开发者工具打开任意页面 → Network → 点击请求 → Response Headers → 确认只存在一个
X-Frame-Options,值为你中间件设的值 - 额外验证:建一个测试 HTML 文件,含
<iframe src="%E4%BD%A0%E7%9A%84%E5%9F%9F%E5%90%8D/login"></iframe>,加载后应显示空白或报错(取决于浏览器),而非渲染出登录页
真正麻烦的是共存逻辑:如果你同时启用 Content-Security-Policy: frame-ancestors(推荐用于现代浏览器),X-Frame-Options 仅作 IE10/11 兜底,二者必须语义一致(如都设为 DENY / 'none'),且不能在同一个响应里混用 SAMEORIGIN 和 'self' —— 表面无错,实则增加维护熵值,且容易在灰度发布时漏同步。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











