需在 handler 开头用 header().set() 设置安全头,避免被中间件覆盖;csp 需严格语法并防拼接注入;http/2 下须用 http.setcookie 而非 header().add;中间件设头必须在 next.servehttp 前。

如何用 http.ResponseWriter 设置自定义响应头
Go 标准库的 http.ResponseWriter 不支持直接“替换”或“覆盖”已有头,它内部用的是惰性写入机制:只要还没调用 Write 或 WriteHeader,你反复调用 Header().Set() 都只影响内存里的 map;一旦响应开始写出(比如第一次 Write),头就锁死了。
常见错误是以为 Header().Set("X-Frame-Options", "DENY") 调一次就行,结果被中间件、路由框架或日志中间件悄悄覆盖了——尤其是用了 gorilla/mux 或 chi 时,它们可能在 handler 执行后又写了头。
- 务必在 handler 函数最开头设置关键安全头,越早越好
- 用
Header().Set()而非Header().Add(),避免重复值(如多个Content-Security-Policy) - 如果依赖第三方中间件,检查它是否调用
WriteHeader(0)或提前写头(有些日志中间件会)
示例:
func myHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
// ⚠️ 此处不能再调用 w.WriteHeader() 以外的头操作
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
为什么 Content-Security-Policy 容易失效
这个头对语法敏感,且浏览器策略执行严格:一个拼写错误、少个引号、空格不合法,整条策略就被忽略。Go 里用字符串拼接生成 CSP 是高危操作,尤其当需要动态插入 nonce 或哈希时。
典型现象:本地开发看着生效,上线后 Chrome DevTools 的 Security 标签页里显示 “Content Security Policy delivered but not enforced”,或者控制台没报 CSP 违规,但实际脚本照常执行。
立即学习“go语言免费学习笔记(深入)”;
- 确保值用单引号包裹指令(如
"script-src 'self' https://cdn.example.com"),双引号只用于整个字符串 - 避免在值中拼接用户输入;若需动态 nonce,用
crypto/rand生成,再注入到模板或响应体中 - 开发期加
Content-Security-Policy-Report-Only头观察违规报告,再切到正式头 - 注意 Go 的
http.ServeFile和静态文件服务默认不设 CSP,得单独包装 handler
HTTP/2 下 Set-Cookie 的特殊处理
Go 1.8+ 默认启用 HTTP/2,而 HTTP/2 禁止在响应头中使用逗号分隔多个 Set-Cookie(HTTP/1.1 允许)。如果你用 Header().Add("Set-Cookie", ...) 多次添加,Go 会自动合并成一行,导致浏览器只解析第一个 cookie。
表现就是:登录成功后只存了 session id,CSRF token cookie 消失了;或者多语言切换后语言偏好没保存。
- 必须对每个 cookie 单独调用
http.SetCookie(w, &http.Cookie{...}) - 不要手动拼
Set-Cookie字符串,绕过http.SetCookie的编码逻辑(如未转义特殊字符) - 注意
http.SetCookie会自动调用w.Header().Add(),所以它本身是安全的 - 若用 fasthttp 等替代库,行为可能不同,得查清其 cookie 写入逻辑
中间件中修改响应头的时机陷阱
很多开发者把安全头塞进中间件,但顺序错了就白忙活。比如在 next.ServeHTTP 之后设置头,此时响应已写出,Header().Set() 完全无效,还可能触发 http: superfluous response.WriteHeader call 错误。
另一个坑是 panic 恢复中间件:如果 handler panic,恢复逻辑里再写头,往往已经 write header 了(Go 默认 200),此时再设头也无效。
- 安全头中间件必须放在链最前面,且在
next.ServeHTTP之前设置 - 如果要兜底加头(比如所有路径都强制 HSTS),用
http.StripPrefix包裹前再加一层中间件 - 不要依赖 defer 在 panic 后补头,改用专门的 error handler 中间件统一处理
- 调试时用
curl -I或 Wireshark 看原始响应,别只信浏览器开发者工具的“Headers”面板(它有时缓存或美化过)
真正麻烦的从来不是写几行 Header().Set(),而是搞清谁在什么时候写了什么头、有没有被覆盖、有没有被协议限制、有没有被中间件劫持——这些地方一漏,安全配置就形同虚设。










