gin 的 c.formfile 不处理重命名,文件名由 file.saveas() 的路径参数决定;需提取并校验扩展名、用 uuid 或时间戳+随机数生成安全新名,避免目录遍历与冲突。

上传时直接重命名文件名
Gin 的 c.FormFile 只负责读取表单中的文件字段,不处理保存逻辑。真正决定文件名的地方是调用 file.SaveAs() 时传入的路径参数——也就是说,重命名完全由你构造的目标路径控制,和 Gin 本身无关。
常见错误是试图修改 *multipart.FileHeader.Filename 字段,这无效:它只是原始上传名的只读快照,改了也不影响实际保存行为。
- 从
c.FormFile("file")获取*multipart.FileHeader - 生成新文件名(如用
uuid.New().String()+ 原扩展名) - 拼接完整保存路径,例如
./uploads/ + newFilename - 调用
file.SaveAs(savePath),此时文件就以新名字落地了
保留原扩展名但防止恶意文件名
用户上传的 Filename 可能含路径遍历(../../etc/passwd)或危险后缀(.php、.exe)。不能直接拼接使用。
安全做法是:提取原始文件扩展名,白名单校验,再组合新名。Gin 不提供内置校验,得自己写。
- 用
filepath.Ext(file.Filename)提取扩展名(注意返回值带点,如".png") - 检查扩展名是否在允许列表中,例如
[]string{".jpg", ".jpeg", ".png", ".pdf"} - 丢弃原始文件名全部内容,仅保留安全扩展名参与新文件名构造
- 避免用
filepath.Join()拼接用户输入的完整路径,防止目录穿越
并发上传时避免文件名冲突
单纯用时间戳(time.Now().UnixNano())在高并发下仍有极小概率重复;UUID 虽好,但部分场景需要可读性(比如按日期分目录)。推荐组合策略:
- 前缀用
time.Now().Format("20060102")控制目录层级(如./uploads/20240520/) - 主体用
uuid.New().String()[:8]保证唯一性 - 后缀严格使用校验后的扩展名,如
"20240520/a1b2c3d4.png" - 确保上传目录存在:
os.MkdirAll(dirPath, 0755),否则SaveAs会报no such file or directory
为什么不用 Gin 的 c.SaveUploadedFile?
c.SaveUploadedFile 只是 file.SaveAs() 的封装,内部完全一样。它不提供重命名能力,也不做安全过滤——传什么路径,就存什么路径。很多人误以为它是“高级接口”,其实只是少写一行解构代码。
它的签名是 func (c *Context) SaveUploadedFile(fh *multipart.FileHeader, dst string) error,其中 dst 就是你必须手动构造的新路径。所以该函数和手动调用 fh.SaveAs(dst) 行为完全等价,选哪个纯看代码风格偏好。
真正容易被忽略的是:所有路径拼接都应在服务端完成,且必须做扩展名校验与目录隔离——哪怕前端传了 avatar.jpg,后端也绝不能信任这个字符串的任意部分。











