Gin默认不自动设置任何安全响应头,必须显式配置X-Content-Type-Options、X-Frame-Options、Content-Security-Policy、Strict-Transport-Security(仅HTTPS)、Referrer-Policy和Cross-Origin-Opener-Policy等关键头,统一通过全局中间件注入,并需单独加固静态资源服务。

Gin 默认不自动设置任何安全响应头,所有关键防护头都得手动加——漏一个就可能被利用。
哪些响应头必须显式设置
浏览器不会默认启用防护机制,以下头必须在中间件或 handler 中主动写入:
-
X-Content-Type-Options: nosniff—— 防止浏览器 MIME 嗅探,避免.js被当成text/html执行 -
X-Frame-Options: DENY或SAMEORIGIN—— 阻止页面被嵌入 iframe,防点击劫持 -
Content-Security-Policy: default-src 'self'—— 最小化资源加载范围,防 XSS 注入执行 -
Strict-Transport-Security: max-age=31536000; includeSubDomains—— 仅 HTTPS 下设置,强制后续请求走 HTTPS -
Referrer-Policy: no-referrer-when-downgrade—— 防止 Referer 泄露敏感路径参数 -
Cross-Origin-Opener-Policy: same-origin—— 隔离跨源 opener,防 tab 接口劫持
注意:Strict-Transport-Security 若在 HTTP 环境下发,浏览器会直接忽略;Content-Security-Policy 若含 unsafe-inline 或 unsafe-eval,等于形同虚设。
怎么在 Gin 中统一注入这些头
用全局中间件最稳妥,确保每个响应都带上(包括静态文件、JSON、HTML):
func securityHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-Frame-Options", "DENY")
c.Header("Content-Security-Policy", "default-src 'self'")
c.Header("Referrer-Policy", "no-referrer-when-downgrade")
c.Header("Cross-Origin-Opener-Policy", "same-origin")
if c.Request.TLS != nil { // 只在 HTTPS 请求中加 HSTS
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
c.Next()
}
}
注册时放在 r := gin.Default() 之后、任何 r.GET 之前:
r.Use(securityHeaders())
别在 handler 里零散写——容易遗漏,也难维护。如果用了 gin-contrib/cors,它的中间件会在你之前执行,所以你的安全头中间件要排在它后面,否则可能被覆盖。
静态文件服务的特殊处理
用 r.StaticFS 或 http.FileServer 提供静态资源时,Gin 不会自动调用中间件,安全头会丢失:
- 不要直接用
r.Static("/static", "./static")—— 它绕过所有中间件 - 改用
r.Use(securityHeaders)+r.StaticFS("/static", http.Dir("./static")),但注意StaticFS仍不触发中间件 - 正确做法是封装一层 handler:用
http.StripPrefix+ 自定义 wrapper,像这样:
func secureStatic(dir string) http.Handler {
fs := http.FileServer(http.Dir(dir))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 先写安全头
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
// 拒绝访问隐藏文件
if strings.HasPrefix(r.URL.Path, "/.") {
http.NotFound(w, r)
return
}
fs.ServeHTTP(w, r)
})
}
// 然后注册:
r.Use(securityHeaders) // 这个对 API 生效
http.Handle("/static/", http.StripPrefix("/static/", secureStatic("./static")))
这里有两个独立作用域:API 走 Gin 中间件,静态资源走标准 http.ServeMux,必须分别加固。
为什么有些头设置了却没生效
常见失效原因不是代码写错,而是被更高优先级逻辑覆盖:
-
c.Header()写在c.Abort()或c.String()之后 —— 响应已提交,头无效 - 用了
gin-contrib/cors且没关掉它的AllowAllOrigins,它内部会重写Access-Control-Allow-Origin,可能冲掉你设的其他头 - 反向代理(如 Nginx)配置了
add_header,且未设always,导致 304 或错误响应不带安全头 - 前端发的是
OPTIONS预检请求,而你的中间件只对GET/POST生效,漏掉了预检响应
验证方式很简单:用 curl -I http://localhost:8080/any-path 看响应头,别只信浏览器开发者工具——它有时会缓存旧头。


















