

新闻资讯
技术学院http.ServeFile 存在路径遍历和缺乏业务控制风险,应手动校验路径、流式读取并设置兼容性 Content-Disposition 头,同时调优服务器超时配置以支持大文件下载。
http.ServeFile 会出问题吗?会。直接用 http.ServeFile 暴露文件路径,容易触发路径遍历(如 ../../etc/passwd),且无法统一控制鉴权、日志、限速等逻辑。它只适合静态资源托管,不适合带业务逻辑的下载接口。
正确做法是手动读取文件并写入 ResponseWriter,自己把控路径合法性与响应头。
核心是三步:校验路径、打开文件、设置响应头后流式写入。重点在于路径必须绝对化、限制根目录、拒绝非法字符。
filepath.Abs 和 filepath.Join 构造完整路径,再用 strings.HasPrefix 确保不越界..、/./、空字节等危险片段的原始文件名os.Open 而非 ioutil.ReadFile,避免大文件 OOMdefer f.Close(),否则句柄泄漏func downloadHandler(w http.ResponseWriter, r *http.Request) { filename := r.URL.Query().Get("name") if filename == "" { http.Error(w, "missing name", http.StatusBadRequest) return } // 白名单校验 + 路径净化 if strings.Contains(filename, "..") || strings.HasPrefix(filename, "/") { http.Error(w, "invalid filename", http.StatusBadRequest) return } absPath := filepath.Join("/var/uploads", filename) if !strings.HasPrefix(absPath, "/var/uploads") { http.Error(w, "access denied", http.StatusForbidden) return } f, err := os.Open(absPath) if err != nil { http.Error(w, "file not found", http.StatusNotFound) return } defer f.Close() // 设置 Content-Disposition 强制浏览器下载 w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`) w.Header().Set("Content-Type", "application/octet-stream") // 流式拷贝,不加载全文到内存 io.Copy(w, f) }
Content-Disposition 的 filename 为什么有时乱码?因为 RFC 5987 规定非 ASCII 文件名需用 filename*=UTF-8''... 编码格式,而老浏览器只认 filename。直接拼接中文会导致部分客户端解析失败或截断。
稳妥做法是:ASCII 名字走 filename,非 ASCII 名字走 filename*,两者都设(兼容性最佳)。
url.PathEscape 编码 UTF-8 字节序列filename* 值中不能有双引号,需先去除mime.WordEncoder —— 它生成的是 RFC 2047 格式,不适用于 Content-Disposition
func setDownloadHeader(w http.ResponseWriter, filename string) {
w.Header().Set("Content-Type", "application/octet-stream")
if isASCII(filename) {
w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)
} else {
encoded := url.PathEscape(filename)
w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"; filename*=UTF-8''`+encoded)
}
}
常见原因不是代码逻辑,而是 HTTP 中间件(如 Nginx、Cloudflare)或 Go 默认的 http.Server 配置限制了超时或缓冲区。
http.Transport 的响应体自动解压(如果用了反向代理)http.Server 中显式设置 ReadTimeout、WriteTimeout 和 IdleTimeout(至少 30 分钟)io.LimitReader 包裹文件 reader,而不是整个 response body真正难处理的是断点续传 —— 如果没实现 Range 请求支持,客户端重试就会从头开始。除非明确要求,否则别自行实现;用成熟 CDN 或对象存储更可靠。