现代浏览器已弃用x-frame-options,必须同时设置x-frame-options与content-security-policy的frame-ancestors且语义一致;安全响应头须在c.next()前一次性设置,https下hsts需谨慎启用并灰度验证,所有配置须经devtools实际验证。

为什么只设 X-Frame-Options 会失效
Chrome 90+、Firefox 92+、Safari 15.4+ 已完全忽略 X-Frame-Options,转而只认 Content-Security-Policy 中的 frame-ancestors。如果你中间件里只写 c.Header("X-Frame-Options", "DENY"),现代浏览器根本不会执行防嵌套逻辑——攻击者仍能用透明 iframe 套住你的登录页。
必须同时设置两者,且语义一致:
-
X-Frame-Options: DENY→ 兼容老浏览器(IE、旧版 Android WebView) -
Content-Security-Policy: frame-ancestors 'none'→ 现代浏览器唯一生效项,单引号不可省,写成frame-ancestors none是无效语法,整条头被浏览器丢弃 - 若需允许同源嵌入,两个头都得设为
SAMEORIGIN和frame-ancestors 'self'
gin.HandlerFunc 中安全头必须在 c.Next() 前设置
Gin 的 c.HTML()、c.Redirect()、c.JSON() 内部会重置或覆盖响应头。常见错误是中间件里设了 Content-Security-Policy,但在 handler 里调用 c.HTML() 后,CSP 就没了。
正确做法:
- 所有安全头必须在
c.Next()之前一次性写完 - 不要在 handler 里补头;也不要在
c.Next()之后调用c.Header(),此时响应可能已写出 - 静态文件路径(如
/static/)默认不走中间件,需单独注册:用r.Use(securityMiddleware)全局注册,再对r.StaticFS("/static", fs)手动包装一个带头的http.FileSystem
HTTPS 下必须加 Strict-Transport-Security,但别乱开 includeSubDomains
HSTS 不是“开了就更安全”,它是一把双刃剑。一旦浏览器收到 Strict-Transport-Security 头,就会强制后续所有请求走 HTTPS,且该策略由浏览器本地缓存,服务端无法撤回。
关键约束:
- 只在 HTTPS 请求中设置:检查
c.Request.TLS != nil或c.GetHeader("X-Forwarded-Proto") == "https" -
includeSubDomains必须确保所有一级子域(如api.example.com、cdn.example.com)已部署有效 HTTPS 证书,否则用户将无法访问这些子域 - 首次上线建议先用
max-age=300(5 分钟)灰度验证,确认无误再逐步延长
例外路径(如 /embed/)要精确放行,不能靠注释绕过
有些页面确实需要被第三方嵌入(比如数据看板),但全局放开 frame-ancestors 就等于放弃防护。不能用“这个接口不重要”来 justify 安全降级。
安全放行方式:
- 用
strings.HasPrefix(c.Request.URL.Path, "/embed/")判断路径前缀,避免误匹配/embeddings这类干扰路径 - 对匹配路径,
X-Frame-Options改为SAMEORIGIN(注意大小写,sameorigin无效) -
Content-Security-Policy改为frame-ancestors 'self' https://trusted-customer.com,每个源必须带单引号、空格分隔 - 禁止使用通配符如
https://*或'unsafe',它们在 CSP 中不被支持
最常被忽略的一点:安全头不是“设了就完事”。你得真去浏览器 DevTools 的 Network 标签页里点开任意响应,展开 Headers → Response Headers,逐条确认 X-Frame-Options、Content-Security-Policy、Strict-Transport-Security 是否真实存在、值是否正确、是否出现在 404 或 302 响应里——很多问题只在非 200 状态码下暴露。











