服务器能拿到文件名,靠的是form表单里<input type="file">标签的name属性,以及multipart/form-data编码类型下请求头中Content-Disposition字段携带的filename参数。前端把文件交出去,后端从请求体里拆包,文件名自然就露出来了,整个过程不神秘,但细节里全是坑。
form上传文件时,文件名是怎么塞进请求里的
要搞清楚服务器怎么得到文件名,先得知道文件从浏览器出发时带了什么“行李”,这个过程分三步,每一步都决定了服务器能不能顺利读到名字。
form的enctype属性决定一切
多数人写上传表单时,只记得加type="file",忘了enctype,这个属性默认值是application/x-www-form-urlencoded,它把表单数据编码成键值对,只适合普通文本字段,传文件时必须改成multipart/form-data,这样浏览器才会把整个请求体拆成多个数据块,每个块里包含字段名、文件名和文件内容。
没有enctype="multipart/form-data",服务器收不到文件名,只能收到一串乱码或者空值。
<form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="myFile"> <button type="submit">上传</button> </form>
input标签的name属性就是文件名的钥匙
服务器解析上传请求时,并不会去读HTML页面里的<input>标签,它只认请求体里的数据,但前端这端,name属性值决定了文件在请求体里的“字段名”,后端通过这个字段名去取文件对象,再从对象里取文件名。
行业共识认为,后端框架几乎都是靠name属性值定位文件的,如果你在<input>上写了name="myFile",Java后端就用request.getPart("myFile")去拿,Node.js的multer里配置upload.single("myFile"),PHP里用$_FILES["myFile"]去读,名字对不上,后端直接报错或拿到null。
multipart数据流里文件名藏在哪
当浏览器发送上传请求时,Content-Type会变成类似这样的值:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
请求体被boundary分隔成多个部分,文件那一块的头部长这样:
Content-Disposition: form-data; name="myFile"; filename="report.pdf"
Content-Type: application/pdf
文件名就挂在Content-Disposition里的filename参数后面。 服务器要做的,就是解析这段头信息,把filename的值拿出来,至于拿完是存原名还是重命名,那是后话,但来源就是这里。
不同后端语言怎么解析这个文件名
前端把文件名送出来了,后端怎么“接住”?不同语言、不同框架,解析路径不一样,但原理一致:解multipart,读头部,取filename。
java form表单获取上传文件名:getSubmittedFileName()
Java原生Servlet处理上传,从HttpServletRequest
拿Part对象,调用getSubmittedFileName()就能得到文件名,这是Servlet 3.1以后提供的方法,直接返回String类型的文件名。
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
Part filePart = request.getPart("myFile");
String fileName = filePart.getSubmittedFileName();
System.out.println("文件原始名: " + fileName);
}
}
如果你的项目还在用老版本Servlet,没有这个方法,可以手动从Part的getHeader("Content-Disposition")里截取filename=后面的值,但要注意处理带引号、带路径的兼容情况,用Spring Boot的话,MultipartFile接口里getOriginalFilename()就是干这事的。
formdata获取文件name属性:前端手动组装也别掉链子
有些场景不靠form表单原生提交,而是用JavaScript的FormData对象手动组装,配合fetch或Axios发请求。
let formData = new FormData();
formData.append("myFile", fileInput.files[0], fileInput.files[0].name);
fetch("/upload", {
method: "POST",
body: formData
});
这里有个小坑:append方法的第三个参数可以手动指定文件名,如果不传,浏览器默认用fileInput.files[0].name,但当你用JS动态处理文件时,比如裁剪图片后再上传,文件名可能变成blob这种,这时候服务器拿到的就是blob而不是原始名。前端组FormData时,第三个参数最好显式传一下文件名。
node.js接收form表单文件:multer中间件帮你拆包
Node.js里最常用的是multer中间件,它内部会用busboy解析multipart数据流,文件名同样从Content-Disposition里提取。
const multer = require("multer");
const upload = multer({ dest: "uploads/" });
app.post("/upload", upload.single("myFile"), (req, res) => {
console.log(req.file.originalname); // 原始文件名
console.log(req.file.filename); // multer重命名后的文件名
res.send("上传成功");
});
req.file.originalname是原始文件名,req.file.filename是multer保存时生成的新文件名(默认是随机字符串加扩展名),注意multer的dest选项只存文件内容,扩展名不会自动保留,需要自己通过originalname解析出来然后重命名存储。
php获取form上传文件名:$_FILES数组里的name字段
PHP的上传处理相对粗暴一些,文件信息直接挂在$_FILES超全局数组里。
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$fileName = $_FILES["myFile"]["name"];
$fileTmpPath = $_FILES["myFile"]["tmp_name"];
echo "上传文件名: " . $fileName;
}
$_FILES["myFile"]["name"]就是原始文件名,tmp_name
是服务器临时存放路径,这里要特别提醒:PHP从$_FILES拿到的文件名是浏览器传过来的原始值,攻击者可以伪造,千万别直接拼路径存盘。
各框架获取上传文件名的API对比
| 语言/框架 | 核心方法/属性 | 获取的是原始名还是处理过 |
|---|---|---|
| Java Servlet 3.1+ | part.getSubmittedFileName() |
原始名 |
| Spring Boot | multipartFile.getOriginalFilename() |
原始名(可能含路径) |
| Node.js multer | req.file.originalname |
原始名 |
| PHP原生 | $_FILES["file"]["name"] |
原始名(可能含路径) |
| Python Flask | request.files["file"].filename |
原始名(可能含路径) |
| Go net/http | r.MultipartForm.File["file"][0].Filename |
原始名 |
表格里几个框架的“原始名(可能含路径)”值得多说一句:老版本IE或某些浏览器会把完整路径C:fakepathreport.pdf传过来,现代浏览器已经统一只传文件名,但后端做处理时最好还是用File.getName()这类方法把路径剥掉,只保留最后一段。
文件名拿到之后,服务器做了什么
文件名到手只是第一步,怎么用这个文件名,才是区分合格后端和菜鸟后端的分水岭。
保留原名还是重命名
多数情况下,服务器不会直接拿原始文件名存盘,原因有三:
- 重名覆盖:两个用户都传
report.pdf,第二个会覆盖第一个 - 路径穿越风险:攻击者把文件名改成
../../../etc/passwd,如果后端直接用这名字拼路径,文件会被写到系统目录 - 中文和特殊字符乱码:不同提交编码下文件名可能变成
%E4%B8%AD%E6%96%87.pdf这种形态,存储和后续访问都麻烦
业界通行做法是:服务器随机生成一个唯一文件名(比如UUID),扩展名从原始文件名里提取后拼接,原始文件名单独存数据库字段关联。
String originalName = filePart.getSubmittedFileName();
String ext = originalName.substring(originalName.lastIndexOf("."));
String storedName = UUID.randomUUID().toString() + ext;
这样既保留了类型信息,又避免了安全和重名问题。
原始文件名要存,但别全信
业内专家指出,原始文件名本质上属于不可信输入,因为它完全由客户端控制,后端接收后在页面回显或下载时,要做HTML转义,防止XSS攻击,比如文件名里带<script>标签,直接输出到页面上就炸了。
下载场景还要注意Content-Disposition头里filename的编码处理,推荐用RFC 5987的filename格式传UTF-8编码的文件名,否则中文名下载时全是乱码。
扩展名白名单校验是什么路线
更稳妥的做法是校验扩展名白名单,不让用户决定文件类型。
- 图片:
jpg, jpeg, png, gif, webp - 文档:
pdf, doc, docx, xls, xlsx - 压缩包:
zip, rar, 7z
后端光看扩展名不够,还要校验Content-Type头(Magic Number),比如JPEG文件开头必须是FF D8 FF,PDF开头必须是%PDF,扩展名可以伪造,文件内容的二进制特征骗不了人。
form上传获取文件名的完整操作流程
把前面拆开的点串起来,从零到一实现一个完整的文件接收+文件名获取流程,跑通一遍就彻底理解了。
如果是Java Spring Boot场景,完整路径是:
- 前端
<form>加enctype="multipart/form-data",<input>设name="myFile" - 后端Controller方法签名里加
@RequestParam("myFile") MultipartFile file - 调用
file.getOriginalFilename()拿原始名 - 解析扩展名,生成UUID作为新文件名
- 调用
file.transferTo(new File("/data/upload/" + newName))存盘 - 把原始名和新名映射写入数据库
如果是Node.js场景,前面4步一样,存盘时用fs.renameSync(req.file.path, "/data/upload/" + newName),因为multer已经先临时存了一份。
关于form上传文件名获取的常见疑问
form上传文件服务器拿到的文件名一定是完整文件名吗?
不一定,现代Chrome、Firefox、Edge都只传文件名本身,不带路径,但老版本IE或某些国产浏览器套壳内核,可能把整个客户机路径传过来,比如C:UsersadminDesktop报告.pdf,后端处理时用路径分隔符(正斜杠或反斜杠)做一次split,取最后一段最稳妥。
多个文件同时上传,每个文件名怎么区分?
<input type="file" name="files" multiple>多选文件时,后端拿到的每个Part里都有自己的filename参数,Java里用request.getParts()遍历拿到所有Part,Node.js里用upload.array("files"),req.files是一个数组,每个元素有自己的originalname,文件之间互不干扰,因为每个文件是独立的multipart数据块,各自的Content-Disposition头携带各自的文件名。
为什么有的场景前端拿到的文件名是blob或是日期字符串?
这是前端主动改的,比如用Canvas把图片压缩后转成Blob再塞进FormData,如果不指定文件名参数,浏览器默认以blob命名,有些组件库内部处理也会生成时间戳或随机串作为文件名。这不是bug,是前端代码里没传第三个参数导致的,在append里显式写个文件名就能解决。
服务器获取form上传文件名的链路,说到底就是前端在multipart数据块的Content-Disposition头里写入filename,后端解析这个头,框架帮你封装好了,但用的时候要顺手做三件事:校验扩展名白名单、剥离路径只留文件名、重命名存储并关联原始名字,这三步走完,文件上传才算真正稳妥落地。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/751725.html





