防盗链必须校验http请求头referer并精确匹配白名单域名,下载权限需绑定用户会话与资源元数据三重检查,大文件须用c.stream()流式传输且统一错误响应格式。

防盗链必须校验 Referer 且不能只依赖前端传参
盗链本质是第三方网站直接嵌入你的静态资源链接(如图片、视频),浏览器发起请求时会带上 Referer 头。Echo 中不能靠客户端提交的 referer 字段或 query 参数做判断——这些全可伪造。必须从 HTTP 请求头中读取原始 Referer,并严格比对白名单域名。
常见错误是把 c.Request().Header.Get("Referer") 的结果直接做字符串包含匹配(比如用 strings.Contains),这会导致 evil.com/yourdomain.com/image.jpg 这类恶意域名绕过。正确做法是解析该 URL,提取 host 后精确比对:
referer := c.Request().Header.Get("Referer")
if referer != "" {
if u, err := url.Parse(referer); err == nil && u.Host != "" {
// 白名单:[]string{"example.com", "app.example.com"}
allowed := false
for _, domain := range allowedDomains {
if u.Host == domain || strings.HasSuffix(u.Host, "."+domain) {
allowed = true
break
}
}
if !allowed {
return echo.NewHTTPError(http.StatusForbidden, "Referer not allowed")
}
}
}
注意:Referer 在 HTTPS → HTTP 跳转或某些隐私设置下可能为空,需允许空值(比如内部跳转或直接访问资源页)或结合其他策略兜底。
下载权限控制要绑定用户会话与资源元数据,不能只校验 token
单纯验证 JWT 或 session cookie 只能确认“当前用户已登录”,但无法回答“这个用户是否能下载这个文件”。必须在中间件中完成三重检查:用户身份有效性 + 文件存在性 + 权限归属关系。
典型结构是:URL 路径含资源 ID(如 /download/:id),中间件通过 c.Param("id") 查数据库,确认该 file_id 是否属于当前 c.Get("user_id")(或所属团队/项目)。若查不到记录,或状态为 deleted / private,直接返回 404 或 403。
容易踩的坑:
- 未对
:id做类型校验,导致 SQL 注入或 panic(应使用uuid.Parse或整型转换+错误处理) - 权限检查放在 handler 里而非中间件,导致部分 handler 漏掉校验
- 文件路径拼接未限制根目录,可能被
../../../etc/passwd绕过(务必用filepath.Join(uploadsDir, filename)并校验是否在uploadsDir内)
echo.HTTPError 要配合自定义错误处理器统一响应格式
Echo 默认的 echo.NewHTTPError 返回的是纯文本或默认 JSON,和你 API 的统一错误结构(如 {"code":403,"msg":"Forbidden"})不一致。如果每个中间件都手动 c.JSON,容易遗漏或格式不一。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
正确做法是在初始化 Echo 实例后,设置 e.HTTPErrorHandler:
e.HTTPErrorHandler = func(err error, c echo.Context) {
code := http.StatusInternalServerError
message := "Internal Server Error"
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = fmt.Sprintf("%v", he.Message)
}
c.JSON(code, map[string]interface{}{
"code": code,
"msg": message,
})
}
这样所有中间件中调用 echo.NewHTTPError(http.StatusForbidden, "...") 都会自动套进统一格式。注意:此处理器不捕获 panic,panic 需另配 e.Use(middleware.Recover())。
大文件下载别用 c.File(),要用 c.Stream() 配合 io.Copy
c.File() 会把整个文件读进内存再发出去,100MB 的 ZIP 就可能耗光服务内存。真实场景必须流式传输。
正确姿势是打开文件句柄后,用 c.Stream() 设置状态码与 header,再通过 io.Copy 分块写入:
file, err := os.Open(filepath.Join(downloadDir, filename))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "File not found")
}
defer file.Close()
c.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf(`attachment; filename="%s"`, filename))
c.Response().Header().Set(echo.HeaderContentType, "application/octet-stream")
return c.Stream(http.StatusOK, file)
关键点:
- 务必设置
Content-Disposition,否则浏览器可能尝试渲染而非下载 - 不要用
c.Attachment()—— 它内部调用c.File(),仍会加载全量文件 - 若需断点续传(
Range请求),得自己解析c.Request().Header.Get("Range")并用file.Seek(),Echo 不内置支持
权限校验、Referer 检查、流式响应这三个环节只要漏掉一个,防盗链或下载控制就形同虚设。尤其是 Referer 校验逻辑,很容易在开发环境测试时忽略 HTTPS 混合内容或空 Referer 场景,上线后才发现大量合法请求被误杀。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










