必须在客户端流式计算哈希并随文件上传,服务端用 io.TeeReader 边写入边校验,统一小写十六进制格式,严格验证 header 合法性,避免内存溢出与重复 I/O。

上传前本地计算文件哈希值
用户点击上传时,不能等服务端读完文件再校验——那样既慢又浪费带宽。必须在浏览器或客户端提前算好哈希,随文件一起发过去。Go 服务端只做比对,不重复计算。
常见错误是前端用 FileReader 读完整个文件再调 crypto-js,大文件(>100MB)容易卡死或内存溢出。正确做法是分块读取、流式哈希(如用 spark-md5 或 sha.js 的 streaming 模式)。
- 前端传哈希时,统一用小写十六进制字符串,避免大小写比对失败
- HTTP 请求头加
X-Content-MD5或自定义字段如X-File-SHA256,比放 body 里更清晰 - Go 后端接收时别直接信任 header 值——要先验证是否为合法 hex 字符串(长度 32/40/64),否则可能 panic
服务端接收时边读边校验
别把整个文件写入磁盘后再算哈希。用 io.TeeReader 把上传流同时写入文件和哈希计算器,一次 I/O 完成两件事。
示例关键逻辑:
立即学习“go语言免费学习笔记(深入)”;
hash := sha256.New()
tee := io.TeeReader(request.Body, hash)
_, err := io.Copy(fileWriter, tee)
if err != nil {
return err
}
expected := request.Header.Get("X-File-SHA256")
if !strings.EqualFold(expected, hex.EncodeToString(hash.Sum(nil))) {
return errors.New("hash mismatch")
}
-
io.Copy返回实际写入字节数,可用于后续判断是否完整接收(对比Content-Length) - 如果用了 multipart 表单,得从
multipart.Part读取,不能直接用request.Body;且注意Part.Header不含自定义 header,哈希值得从 form field 单独传 - MD5 虽快但不安全,生产环境优先用
sha256或sha512;若兼容旧系统必须用 MD5,至少确保传输走 HTTPS
大文件上传中断后的续传与校验
单纯哈希无法支持断点续传——因为哈希是全局摘要,缺一块就全错。真要支持断点,得改用分块哈希(如 Bittorrent 的 piece hash)或服务端记录已收块再合并校验。
- 简单方案:前端按固定块大小(如 5MB)切片,每片单独计算 SHA256,上传时带
Upload-ID和Chunk-Index,服务端存每个块哈希,最后拼接后整体校验 - 更稳妥的做法是上传完成后,服务端重新 open 文件并计算最终哈希——但这会带来二次 I/O 开销,需权衡
- 注意临时文件权限:
os.O_CREATE | os.O_WRONLY | os.O_EXCL避免竞态覆盖;校验失败时务必 clean 临时文件,不然磁盘悄悄占满
校验失败时的错误反馈要具体
返回 400 Bad Request 时,别只说 “校验失败”。用户需要知道是哪一环出问题:是前端算错了?网络损坏?还是服务端读取异常?
- 日志里记下
expected、actual、content-length、upload-id四个字段,排查时能快速定位 - 前端收到失败响应后,应提示“文件可能损坏,请重新选择”,而不是“上传失败”这种模糊信息
- 某些代理(如 Nginx)会缓冲请求体并修改
Content-Length,导致服务端读取字节数与 header 不符——这种情况下哈希必然失败,得查代理配置而非代码


















