Referer校验必须先解析URL再比对host,不能字符串匹配;需处理空Referer、解析错误、白名单未命中等场景并显式return;静态资源路由需手动注册带校验的GET路由;Referer仅为辅助手段,须配合签名URL、token等多重防护。

Referer 校验必须先解析再比对 host,不能字符串匹配
直接用 strings.Contains(r.Header.Get("Referer"), "example.com") 会漏掉协议差异(https:// vs http://)、端口干扰(example.com:8080)、路径后缀(example.com/login?next=...),甚至被构造为 evil.com.example.com 绕过。浏览器也可能发空 Referer(比如从 HTTPS 页面跳转到 HTTP 资源),硬匹配会把合法请求拦掉。
正确做法是:用 url.Parse() 解析 Referer 字符串,提取 u.Scheme 和 u.Host,去掉默认端口(:80/:443),再与白名单中的纯 host(如 "app.example.com")做精确比对。
- 白名单应为
[]string,每个元素只含 host,不带协议、斜杠或端口 - 空 Referer 必须显式判断:允许、拒绝,或降级走 token 校验(不能忽略)
-
r.Referer()是方法,不是字段,别写成r.Referer—— 这会编译报错
Gin 中间件里校验失败必须显式 return,否则 handler 仍执行
很多人只写 c.AbortWithStatusJSON(...) 就结束,但 Gin 不会自动中断后续逻辑。如果校验失败后没 return,控制流会继续进入业务 handler,导致防盗链形同虚设。
典型错误写法:
立即学习“go语言免费学习笔记(深入)”;
if !allowed {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "referer not allowed"})
// 缺少 return → 后续代码照常执行
}
正确结构必须包含 return:
- 检查
referer == ""后,立即return -
url.Parse()出错或u.Host == ""时,return - 白名单未命中时,
return
静态资源路由(如 /images/*filepath)需单独加中间件
Gin 的 Static 或 StaticFS 默认不走中间件链,它们是底层 http.FileServer 的封装,绕过 Gin 的路由和中间件机制。想对 /images/logo.png 做 Referer 校验,不能靠全局中间件,必须手动注册带校验的路由。
推荐写法:
router.GET("/images/*filepath", func(c *gin.Context) {
referer := c.Request.Header.Get("Referer")
// …… Referer 解析与白名单校验逻辑(同上)
if !allowed {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.FileFromFS(c.Param("filepath"), http.Dir("./images"))
})
- 不要用
router.Static("/images", "./images"),它不触发中间件 -
c.FileFromFS()是安全替代,支持自定义响应头、校验逻辑 - 注意路径参数名必须是
filepath,且路由末尾带*才能匹配子路径
Referer 仅作辅助手段,必须配合其他防护
Referer 头可被客户端任意伪造,攻击者用 curl 或 Postman 轻松绕过。它只能防普通用户误点或简单爬虫,不能替代真正安全机制。
生产环境必须叠加:
- 签名 URL:对静态资源链接加入时效性签名(如
/images/logo.png?sig=abc123&exp=1723456789) - 短期 Token:在 HTML 模板中动态注入一次性 token,服务端校验后再返回资源
- Referer + User-Agent 组合校验:增加伪造成本(但依然不防高级攻击)
- 敏感资源改用 API 接口返回,而非直连静态路径
最容易被忽略的是:白名单域名没做子域名隔离(比如允许 example.com,结果 evil.example.com 也被放行),或者忘了处理空 Referer 场景 —— 这两类问题在真实日志里高频出现。


















