gin 必须显式设置 x-frame-options 或 csp frame-ancestors 响应头来防御点击劫持,因其不内置安全头策略,gin.default() 无任何防护,且前端 frame-buster 可被绕过;中间件需在 c.next() 前调用 c.header() 并避免被 nginx 覆盖,推荐双头共置(deny + frame-ancestors 'none')以兼顾兼容性与灵活性。

直接结论:Gin 必须通过 Header 设置 X-Frame-Options 或 Content-Security-Policy 的 frame-ancestors 指令来防御点击劫持,仅靠前端 JS “frame-buster” 无效且可被绕过。
为什么 Gin 不能只靠中间件自动加 header?
Gin 不内置安全头策略,gin.Default() 完全不设置任何防护响应头。你看到的“默认防 XSS”是误解——X-XSS-Protection 已被现代浏览器弃用,且对 Clickjacking 完全无作用。必须显式调用 c.Header() 才生效。
- 不手动设置 → 响应里没有
X-Frame-Options或frame-ancestors→ 任意网站都能用<iframe src="your-gin-app"></iframe>嵌套你的登录页 - 中间件中漏掉
c.Next()→ 后续 handler 不执行,页面空白但头也没发出去 - 在 handler 里写
c.Header()但位置靠后(比如在c.JSON()之后)→ 头已发送,再设无效
该用 X-Frame-Options 还是 Content-Security-Policy?
两个都设,但优先级和兼容性不同:X-Frame-Options 在旧浏览器(IE、老版 Edge)有效;frame-ancestors 更灵活但 Chrome/Firefox 70+ 才完全支持。注意:若两者同时存在,部分浏览器(如 Safari)会以 X-Frame-Options 为准并忽略 CSP。
-
X-Frame-Options: DENY—— 最严,禁止所有嵌套,包括同域 iframe(适合管理后台、支付页) -
X-Frame-Options: SAMEORIGIN—— 允许同域名 iframe,适合需内嵌子应用的场景(如微前端) -
Content-Security-Policy: frame-ancestors 'none'—— 等效于DENY,但语义更明确,支持多源白名单(如'self' https://embed.example.com') -
ALLOW-FROM已被 Chrome/Safari 废弃,不要用 —— 会直接忽略并报Invalid 'X-Frame-Options' header
实际中间件怎么写才可靠?
下面这个中间件是生产可用的最小可行版本,它处理了 HTTPS 判定、重复 header 风险、以及 CSP 和 XFO 的共存逻辑:
func SecurityHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
// 总是设置 X-Frame-Options(兼容性兜底)
c.Header("X-Frame-Options", "DENY")
// 总是设置 CSP frame-ancestors(现代标准)
c.Header("Content-Security-Policy", "frame-ancestors 'none'")
// 其他基础安全头(可选但推荐)
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-Frame-Options", "DENY") // 再设一次不冲突,add_header 是追加,Header 是覆盖
c.Next()
}
}
- 注册方式:
r.Use(SecurityHeaders()),放在路由注册前 - 不要用
add_header(那是 Nginx 指令),Gin 里只能用c.Header() - 如果某些接口(如公开 embed API)需要允许特定来源,改用
frame-ancestors 'https://trusted.com',别碰ALLOW-FROM - 测试是否生效:打开浏览器 DevTools → Network → 选一个 HTML 响应 → 查看 Response Headers 是否含这两项
容易被忽略的验证盲区
设完头不等于防住了。真实环境中常踩的坑是:
- 反向代理(如 Nginx)覆盖了 Gin 的 header —— 检查 Nginx 配置里有没有
proxy_hide_header X-Frame-Options或未启用proxy_pass_request_headers on - 静态文件(
router.Static())走的是 Gin 文件服务,但中间件默认不触发 —— 必须显式对静态路由加中间件,或改用router.Use()全局挂载 - 前端 SPA 路由(如 Vue Router history 模式)返回的都是
index.html,但如果你用router.NoRoute()返回静态页,那个 handler 也得手动加 header - 本地开发用 HTTP 访问时,
Strict-Transport-Security不生效,但frame-ancestors和X-Frame-Options依然起作用 —— 不要误以为“没上 HTTPS 就不用设”
最稳妥的做法:上线前用 curl 直接请求 HTML 路径,确认响应头里确实存在且值正确。防御点击劫持不是加一行代码就结束的事,而是从响应生成、代理转发、到浏览器解析的全链路控制。











