常见原因是前端未用multipart/form-data编码或后端未调用ctx.ParseMultipartForm预解析;Gin不自动解析,需在读取前显式调用ctx.ParseMultipartForm(32<<20)。

上传PDF文件时为什么ctx.FormFile返回nil
常见原因是前端没用multipart/form-data编码,或后端没调ctx.ParseMultipartForm预解析。Gin不会自动解析大文件表单,默认只处理小量表单字段。必须在读取文件前显式调用:
ctx.ParseMultipartForm(32 << 20) // 32MB上限,单位是字节否则
ctx.FormFile("file")一定返回nil且无错误提示。
如何校验上传文件确实是PDF格式
仅靠Content-Type(如application/pdf)不可信,浏览器可伪造。必须检查文件魔数(magic bytes):
- PDF标准开头是
%PDF-(ASCII码:0x25 0x50 0x44 0x46 0x2D) - 读取文件前4–8字节,比对是否匹配
- 别用
file.Header.Get("Content-Type")做唯一判断
file, header, err := ctx.Request.FormFile("file")<br>if err != nil { return }<br>defer file.Close()<br><br>// 读取前8字节<br>buf := make([]byte, 8)<br>n, _ := file.Read(buf)<br>if n < 4 || string(buf[:4]) != "%PDF" {<br> ctx.AbortWithStatusJSON(400, gin.H{"error": "not a valid PDF"})<br> return<br>}存储PDF到本地磁盘要注意路径与权限
直接拼接用户传的header.Filename有路径遍历风险(比如../../etc/passwd)。必须清洗文件名:
- 用
path.Base()剥离路径部分 - 用正则或白名单过滤非法字符(如
/ \ : * ? " |) - 强制加上
.pdf后缀,防止扩展名欺骗 - 确保目标目录存在且Web进程有写权限(Linux下常需
chown www-data:www-data uploads/)
./uploads/pdf/2024/06/uuid4.pdf,按日期分目录避免单目录文件过多。为什么用ioutil.WriteFile保存PDF会损坏文件
因为ioutil.WriteFile默认以字符串模式写入,而PDF是二进制流。必须用os.Create + io.Copy流式写入:
dst, err := os.Create(uploadPath)<br>if err != nil { return }<br>defer dst.Close()<br><br>// 从内存/表单流直接拷贝,不经过字符串解码<br>if _, err = io.Copy(dst, file); err != nil {<br> return<br>}若用ctx.SaveUploadedFile,它内部已做正确处理,但注意它不校验PDF魔数,仍需前置校验。
文件上传本质是流处理,PDF又对字节敏感——魔数校验、二进制写入、路径净化这三点漏掉任一,轻则存错文件,重则引发安全问题。



















