MD5校验必须在分片写入磁盘后、合并前由服务端实时流式重算,不可信任客户端传值。因客户端切片可能错位、篡改或截断,须用io.TeeReader边写边算,配合os.O_EXCL防覆盖,并在校验失败时立即清理对应分片。

MD5校验必须在分片写入磁盘后、合并前做,不能靠客户端传来的值直接信任
为什么不能直接用客户端传的 MD5 做校验
客户端计算的 file_id(如文件整体 MD5)或 chunk_hash 可能被篡改、截断、或因浏览器 Blob 切片边界不一致导致错位。尤其当使用 Blob.slice() 时,若起始偏移未对齐底层文件系统块,两次切片结果可能不一致,MD5 就失效了。
常见错误是把 chunk_hash 当作可信输入,直接存进数据库或跳过校验——这会让损坏分片静默通过,最终合并出的文件无法打开。
- 服务端必须自己重算:收到分片后,用
crypto/md5.New()+io.Copy()实时流式计算,不落地再读 - 不要用
os.ReadFile()重读刚写入的分片——它可能还没 flush,或被并发写覆盖 - 若分片路径含
{file_id},别把它当校验依据;file_id应仅作目录名,不参与完整性判断
分片级校验怎么写才可靠
每个分片上传 handler 必须独立完成哈希计算,且与写入过程绑定。关键不是“算得准”,而是“算得及时、不依赖中间状态”。
正确做法是用一个 io.TeeReader 把 r.Body 同时喂给哈希器和文件写入器:
hash := md5.New()
dst, _ := os.OpenFile(filepath.Join(uploadDir, fileID, fmt.Sprintf("chunk_%04d", index)), os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
defer dst.Close()
tee := io.TeeReader(r.Body, hash)
_, err := io.Copy(dst, tee) // 一边写磁盘,一边算 MD5
if err != nil {
return
}
actualHash := hex.EncodeToString(hash.Sum(nil))
- 必须用
os.O_EXCL打开文件,防止并发重复写入覆盖,导致哈希与内容不匹配 - 若客户端传了
X-Chunk-Hash,就比对actualHash;不等则返回 400,删掉刚写的分片 - 别在
io.Copy后再dst.Seek(0, io.SeekStart)重读——dst是文件句柄,不是内存 buffer
合并前要不要再校验一次整体 MD5
要,但不是重新读整个合并后的大文件——那会 OOM 或卡死。应该在合并过程中边读边算。
合并 handler 的逻辑应是:按序打开每个分片 → io.CopyN 写入目标文件 → 同时把每片数据喂给同一个 md5.Hash 实例。这样内存只占单片大小,且哈希值严格对应物理字节流。
- 不要用
os.Stat().Size()验证总长度——分片可能缺失或写入不全,需以实际io.CopyN返回字节数为准 - 若最终哈希与客户端声称的
file_id不一致,说明某片在传输/落盘/合并中出错,此时应返回 500 并清理临时目录 - Redis 或本地
sync.Map中记录的已传分片索引,不能代替哈希校验;它只管“有没有”,不管“对不对”
最常被忽略的一点:校验失败后没清理已写分片。残留的损坏分片下次续传时会被复用,导致错误累积。每次校验不通过,必须同步删除对应 chunk_* 文件,否则断点续传就变成了断点“污染”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











