防盗链校验的核心逻辑是验证 referer 请求头是否来自白名单域名,而非 url 签名或 token;需在静态文件路由前用中间件拦截,解析 referer host 并精确匹配白名单,同时注意 cdn 和跨协议导致的 referer 丢失问题。

防盗链校验的核心逻辑在哪
Go 里实现图片防盗链,本质是验证 Referer 请求头是否来自白名单域名,而不是检查 URL 签名或 Token。很多开发者误以为必须加签,其实 HTTP 层的 Referer 校验足够应对多数静态资源盗链场景,且零额外存储开销。
关键点在于:浏览器发起图片请求时会自动带上 Referer,而直接在地址栏访问、或用 curl 不带 -H "Referer:..." 时该字段为空或非法——这正是可拦截的依据。
-
Referer可被客户端伪造,所以仅用于防小白盗链,不替代鉴权 - 空
Referer(如从 HTTPS 页面加载 HTTP 图片)需明确放行或拦截,不能忽略 - 校验应放在静态文件路由前,避免先读文件再拒绝,浪费 I/O
用 net/http 实现中间件拦截
最轻量的方式是写一个 http.Handler 中间件,在 ServeHTTP 中做判断。不要依赖第三方库,标准库完全够用。
func refererCheck(next http.Handler, allowed []string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
referer := r.Referer()
if referer == "" {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
u, err := url.Parse(referer)
if err != nil || !contains(allowed, u.Host) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
func contains(list []string, s string) bool {
for _, v := range list {
if v == s {
return true
}
}
return false
}
- 注意
r.Referer()返回的是完整 URL 字符串,需用url.Parse提取Host - 白名单用
[]string而非 map,小规模域名下更省内存、无并发安全问题 - 不要用
strings.Contains模糊匹配 Host,防止evil-example.com匹配到example.com
配合 Gin 或 Echo 的注意事项
如果你用 Gin,别把校验写成 gin.HandlerFunc 后塞进 router.StaticFS——Gin 的 StaticFS 不走中间件链。正确做法是用 router.GET("/images/*filepath", handler) 手动接管路径,并用 http.ServeFile 或 http.FileServer 输出文件。
- Gin 中
c.Writer和http.ResponseWriter不完全等价,推荐用http.ServeFile(w, r, filepath)而非c.File(),避免响应头被框架覆盖 - Echo 的
e.File支持自定义echo.HTTPErrorHandler,但 Referer 校验仍需前置中间件,否则静态路由不触发 - 若图片路径含 query 参数(如
/a.jpg?x=1),*filepath会包含?x=1,需用strings.SplitN(r.URL.Path, "?", 2)截断
生产环境必须绕开的坑
本地开发时一切正常,上线后突然大量 403?大概率是 CDN 或反向代理清除了 Referer 头。Nginx 默认透传,但 Cloudflare、阿里云 CDN 等常默认剥离或重写它。
- Cloudflare 需在「规则」中添加“保留 Referer”动作,或关闭“Hotlink Protection”开关(它会覆盖你的逻辑)
- Nginx 如果用了
proxy_set_header Referer "",立刻删掉;若用proxy_set_header Referer $http_referer,确保 client 请求头没被上游过滤 - HTTPS → HTTP 的跨协议请求会导致浏览器清空
Referer,此时要么全站 HTTPS,要么白名单显式允许空 Referer(慎用)
真正难处理的不是代码,而是 Referer 在多层代理间的生存状态——它不像 Authorization 那样被广泛尊重,调试时优先抓包看原始请求头是否到达 Go 进程。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











