不能信任客户端传来的filename,因其可被任意伪造,直接拼接会导致文件覆盖、路径遍历和字符异常;应使用服务端生成的唯一文件名,推荐基于文件内容sha256哈希(中小文件)或时间戳+随机数(大文件),扩展名须从content-type或魔数提取,严禁截取原始名。

直接用唯一文件名覆盖原始文件名,别依赖用户传来的 filename —— 这是最简单也最有效的防重名手段。
为什么不能信任 c.FormFile("file").Filename
用户上传时可以任意伪造表单字段,Filename 字段完全由客户端控制,不具可信性。常见错误是直接拼接路径:./uploads/ + file.Filename,结果导致:
- 同名文件被覆盖(如两个用户都传
avatar.jpg) - 路径遍历攻击(如传
../../etc/passwd) - 空格、中文、特殊字符引发读写异常
推荐做法:生成服务端唯一文件名
用哈希 + 时间戳 + 随机数组合,彻底脱离原始名。关键点:
- 不要用
uuid.New().String()单独作为文件名(碰撞概率低但非零,且无内容一致性校验) - 推荐用
sha256.Sum256(fileHeader.Open()).Sum(nil)计算文件内容哈希(适合中小文件) - 大文件慎用全量哈希,可改用分块哈希或
time.Now().UnixNano()+rand.Intn(999) - 扩展名必须从
fileHeader.Header.Get("Content-Type")或魔数检测提取,不能从原始名截取
示例片段:
file, err := c.FormFile("file")
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": "no file received"})
return
}
src, _ := file.Open()
defer src.Close()
hash := sha256.New()
io.Copy(hash, src)
digest := fmt.Sprintf("%x", hash.Sum(nil))
ext := filepath.Ext(file.Filename)
if ext == "" {
ext = ".bin" // fallback
}
newName := fmt.Sprintf("%s%s", digest[:16], ext) // 截取前16位避免过长
dst := filepath.Join("./uploads", newName)
if err := c.SaveUploadedFile(file, dst); err != nil {
c.AbortWithStatusJSON(500, gin.H{"error": "save failed"})
return
}
额外要堵的坑:目录隔离与清理
仅改名还不够,真实项目中容易忽略这两点:
- 所有上传文件统一进
./uploads/目录,禁止用用户可控字段构造子路径(如./uploads/ + userID)—— 除非你做了完整路径白名单校验 - 临时分片文件、合并失败的中间文件必须有超时清理机制(比如用
filepath.Walk扫描*.part文件,删除 24 小时前的) - 如果支持断点续传,每个文件的分片应存到以文件哈希为名的子目录下(如
./uploads/ab12cd34/001.part),避免不同文件分片混在一起
真正难的不是生成唯一名,而是确保这个“唯一”在并发上传、分片合并、失败回滚等场景下依然成立 —— 哈希值本身是内容指纹,比时间戳更可靠,但代价是 IO 开销。权衡点就在这里。











