// Package middleware — hotlink.go 下载防盗链(26.9): // 校验 Referer 白名单。规则: // - Referer 为空(直接访问/curl/浏览器地址栏):放行(不误伤正常取件); // - Referer 与当前请求 Host 同源:放行; // - Referer 主机命中管理端白名单(hotlink_whitelist,逗号分隔域名,支持 *.example.com 通配):放行; // - 其余一律 403。 package middleware import ( "net/url" "strings" "github.com/gin-gonic/gin" "fileshare/internal/config" ) // HotlinkMiddleware 返回防盗链中间件;cfg 动态读取开关与白名单(管理端改后立即生效)。 func HotlinkMiddleware(cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { if !cfg.HotlinkEnabled() { c.Next() return } ref := c.GetHeader("Referer") if ref == "" { c.Next() // 空 Referer 放行 return } u, err := url.Parse(ref) if err != nil || u.Host == "" { c.Next() // 非法 Referer 视同空,放行(避免误伤) return } if strings.EqualFold(u.Host, c.Request.Host) { c.Next() // 同源放行 return } if hostAllowed(u.Host, cfg.HotlinkWhitelist()) { c.Next() return } c.AbortWithStatusJSON(403, gin.H{"message": "防盗链:外部站点引用不允许访问该资源"}) } } // hostAllowed 判断主机是否命中白名单(精确匹配或 *. 通配后缀匹配)。 // 白名单条目可带端口;通配写作 .example.com 或 *.example.com。 func hostAllowed(host string, whitelist []string) bool { if len(whitelist) == 0 { return false } host = strings.ToLower(host) for _, w := range whitelist { w = strings.ToLower(strings.TrimSpace(w)) w = strings.TrimPrefix(w, "*") // *.example.com → .example.com if w == "" { continue } if host == strings.TrimPrefix(w, ".") { return true } if strings.HasSuffix(host, w) && strings.HasPrefix(w, ".") { return true } } return false }