http.ServeFile 存在路径遍历和缺乏业务控制风险,应手动校验路径、流式读取并设置兼容性 Content-Disposition 头,同时调优服务器超时配置以支持大文件下载。

用 http.ServeFile 会出问题吗?
会。直接用 http.ServeFile 暴露文件路径,容易触发路径遍历(如 ../../etc/passwd),且无法统一控制鉴权、日志、限速等逻辑。它只适合静态资源托管,不适合带业务逻辑的下载接口。
正确做法是手动读取文件并写入 ResponseWriter,自己把控路径合法性与响应头。
如何安全读取并返回文件?
核心是三步:校验路径、打开文件、设置响应头后流式写入。重点在于路径必须绝对化、限制根目录、拒绝非法字符。
- 用
filepath.Abs和filepath.Join构造完整路径,再用strings.HasPrefix确保不越界 - 拒绝含
..、/./、空字节等危险片段的原始文件名 - 用
os.Open而非ioutil.ReadFile,避免大文件 OOM - 务必调用
defer 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。直接拼接中文会导致部分客户端解析失败或截断。
立即学习“go语言免费学习笔记(深入)”;
稳妥做法是: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 分钟) - 避免在 handler 中做耗时计算(如 ZIP 打包、加解密),应提前生成好文件或用协程异步处理
- 如需限速,用
io.LimitReader包裹文件 reader,而不是整个 response body
真正难处理的是断点续传 —— 如果没实现 Range 请求支持,客户端重试就会从头开始。除非明确要求,否则别自行实现;用成熟 CDN 或对象存储更可靠。










