- 后端
- 微服务
- 云原生
【免费下载链接】midway
🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈
文件上传是 Web 应用中最高频的需求之一。本文以 Midway Hooks 的官方文档 hooks/upload.md 为核心骨架,讲解如何在 Midway Hooks 的"纯函数(pure function)+ 一体化项目"架构下,通过@midwayjs/hooks-upload配合@midwayjs/upload组件,快速实现可复用的文件上传接口。读完本文,你将掌握组件安装与启用、Upload(path)接口声明、useFiles()/useFields()取值、前后端集成调用与手动 FormData 调用,以及上传模式、白名单、MIME 校验、临时文件清理等配置与安全要点。
一、方案概览:为什么需要两个包
Midway Hooks 提供了@midwayjs/hooks-upload,并与@midwayjs/upload协作完成文件上传功能:
@midwayjs/upload:通用上传组件,负责底层multipart/form-data请求的解析、扩展名白名单校验、MIME 类型校验、临时文件落盘与清理等能力,同时兼容@midwayjs/koa、@midwayjs/faas、@midwayjs/web、@midwayjs/express等多套框架(详见 extensions/upload.md)。@midwayjs/hooks-upload:面向 Hooks 的封装层,提供Upload(path)装饰器与useFiles()、useFields()等 Hooks 风格 API,让文件上传接口与普通函数接口一样声明式、可调用。
两者配合后,前端可以直接像调用普通函数一样调用上传接口,也可以使用原生FormData手动上传,后端则始终以纯函数的方式编写逻辑。
二、安装依赖
在项目根目录执行:
npm install @midwayjs/upload @midwayjs/hooks-upload其中@midwayjs/upload当前仓库内的版本为4.2.5(见 packages/upload/package.json),其对 Node.js 的引擎要求为>=20,运行时依赖file-type与raw-body两个 npm 包。
三、启用上传组件
在后端目录的configuration.ts中启用@midwayjs/upload组件。在imports数组中加入upload即可:
import { createConfiguration, hooks } from '@midwayjs/hooks'; import * as Koa from '@midwayjs/koa'; import * as upload from '@midwayjs/upload'; /** * setup midway server */ export default createConfiguration({ imports: [ Koa, hooks(), upload, ], importConfigs: [{ default: { keys: 'session_keys' } }], });组件被导入后,其内部的UploadConfiguration会读取upload命名空间的配置,并在onReady阶段自动把UploadMiddleware挂载到 koa、faas、express、egg 等应用实例上(见 packages/upload/src/configuration.ts)。也就是说,一旦启用,所有命中条件的multipart/form-data请求都会自动进入上传解析流程。
四、创建上传接口
在后端目录新建一个接口文件,使用Api()包裹Upload('/api/upload')声明上传路由,再在函数体内通过useFiles()获取上传文件:
import { Api } from '@midwayjs/hooks'; import { Upload, useFiles, } from '@midwayjs/hooks-upload'; export default Api( Upload('/api/upload'), async () => { const files = useFiles(); return files; } );这里的关键点是Upload(path?: string)装饰器:它把该接口声明为上传接口,可指定路由路径(默认POST),并且只支持multipart/form-data类型的请求。函数体内部没有ctx、没有req,文件数据以 Hooks 的方式直接注入,这正是 Midway Hooks"纯函数"风格的体现。
五、前端调用
5.1 集成调用(推荐)
在一体化项目中,前端可以像调用普通函数一样直接import这个接口并传入files字段。以下是一个 React 表单示例:
import upload from './api/upload'; function Form() { const [file, setFile] = React.useState<FileList>(null); const handleSubmit = async ( e: React.FormEvent<HTMLFormElement> ) => { e.preventDefault(); const files = { images: file }; const response = await upload({ files, }); console.log(response); }; const handleOnChange = ( e: React.ChangeEvent<HTMLInputElement> ) => { console.log(e.target.files); setFile(e.target.files); }; return ( <form onSubmit={handleSubmit}> <h1>Hooks File Upload</h1> <input multiple type="file" onChange={handleOnChange} /> <button type="submit"> Upload </button> </form> ); }注意传入的files是一个对象,key 就是表单字段名(例如上面的images),value 是FileList。该 key 在后端会原样成为useFiles()返回对象的 key。
5.2 手动调用(FormData 上传)
如果不想借助 Hooks 客户端,也可以直接使用浏览器原生FormData+fetch请求上传接口:
const input = document.getElementById('file'); const formdata = new FormData(); formdata.append('file', input.files[0]); fetch('/api/upload', { method: 'POST', body: formdata, }) .then((res) => res.json()) .then((res) => console.log(res));由于接口是通过Upload('/api/upload')声明的,/api/upload接受POST + multipart/form-data,fetch时只要把FormData作为 body 即可,无需手动设置Content-Type(浏览器会自动附带boundary)。
六、Hooks API 详解
6.1Upload(path?: string)
声明上传接口。path可指定路由路径;默认生成POST接口,且仅接受multipart/form-data类型请求。
6.2useFiles()
在函数体内调用,用于获取上传的文件。返回值是Object:
- key 为上传时的字段名(field name);
- 当多个文件使用相同的字段名时,value 为数组(Array)。
// frontend await upload({ pdf }); // backend const files = useFiles(); { pdf: { filename: 'test.pdf', // file original name Data: '/var/tmp/xxx.pdf', // temporary file address of the server when mode is file fieldname: 'test1', // form field name mimeType: 'application/pdf', // mime } }从底层数据结构看,单个文件对象的完整字段在 packages/upload/src/interface.ts 中定义为UploadFileInfo<T>:filename(文件原始名)、fieldName(表单字段名)、mimeType(MIME 类型)、data(mode为file时是服务器临时文件地址字符串,mode为stream时是Readable流)。同时,解析后的文件对象上还会被挂上一个 Symbol 类型的扩展名标记EXT_KEY(见 packages/upload/src/constants.ts),供后续落盘命名使用。
6.3useFields()
返回FormData中非文件部分的字段(普通表单字段):
// frontend const formdata = new FormData(); formdata.append('name', 'test'); post(formdata); // backend const fields = useFields(); // { name: 'test'}如果启用了allowFieldsDuplication,重名字段会被合并为数组,例如{ name: ['name1', 'name2'] }。
七、底层实现:请求如何被解析
了解底层流程有助于排查问题和合理配置。核心逻辑在 packages/upload/src/middleware.ts 的UploadMiddleware.execUpload中,大致流程如下:
- 判定是否为上传请求:
getUploadBoundary()检查请求method是否为POST/PUT/DELETE/PATCH,且content-type以multipart/form-data;开头并携带boundary。不满足则直接next()放行,不影响普通接口。 - 流式解析:当请求体是
ReadableStream且mode === 'stream'时,走parseFromReadableStream流式拆包(见 packages/upload/src/parse.ts),逐 chunk 寻找 boundary,避免大文件整体读入内存。 - 整包解析:
mode === 'file'时,先通过raw-body按fileSize限长读取 body,再由parseMultipart按\r\n--boundary拆块,解析Content-Disposition头中的filename与name,区分文件与普通字段。 - 扩展名校验:
checkAndGetExt()把文件名转小写后逐级截取扩展名,与白名单比对,不匹配则抛出MultipartInvalidFilenameError(400 响应,见 packages/upload/src/error.ts)。 - MIME 校验:若配置了
mimeTypeWhiteList,会通过file-type包读取文件二进制头识别真实 MIME,与规则比对,不通过则抛出MultipartInvalidFileTypeError。 - 落盘或流转:
mode === 'file'时写入临时目录;mode === 'stream'时包装成Readable。随后把fields与files挂到ctx上,供useFiles()/useFields()消费。
组件还向ctx注入了cleanupRequestFiles()方法,用于主动删除当前请求产生的临时文件(见 packages/upload/src/middleware.ts)。
八、配置项详解
@midwayjs/upload的所有配置均挂在upload命名空间下,可在后端config.default.ts中覆盖。默认值定义在 packages/upload/src/config/config.default.ts,类型定义见 packages/upload/src/interface.ts:
| 配置项 | 默认值 | 说明 |
|---|---|---|
mode | 'file' | 上传模式:file(落盘到服务器临时目录)或stream(流式) |
fileSize | '10mb' | 最大上传文件大小(字节/字符串格式) |
whitelist | uploadWhiteList | 允许上传的文件扩展名白名单;设为null则不校验扩展名 |
tmpdir | join(tmpdir(), 'midway-upload-files') | 上传文件的服务器临时存储目录 |
cleanTimeout | 5 * 60 * 1000 | 临时文件自动清理间隔(毫秒),默认 5 分钟;设为0可关闭自动清理 |
base64 | false | 请求体是否为 base64 编码(用于腾讯云 apigw 等场景兼容) |
allowFieldsDuplication | false | 是否允许同名表单字段,开启后合并为数组 |
match/ignore | 无 | 路径匹配规则,match优先级高于ignore |
mimeTypeWhiteList | 无(默认不校验) | 扩展名到 MIME 的映射规则,用于内容级校验 |
一个完整的配置示例:
// src/config/config.default.ts import { uploadWhiteList } from '@midwayjs/upload'; import { tmpdir } from 'os'; import { join } from 'path'; export default { // ... upload: { // mode: 'file' 表示上传到服务器临时目录,也可配置为 'stream' mode: 'file', // 最大上传文件大小,默认 10mb fileSize: '10mb', // 扩展名白名单,这里示例移除 .pdf whitelist: uploadWhiteList.filter(ext => ext !== '.pdf'), // 上传文件的临时存储路径 tmpdir: join(tmpdir(), 'midway-upload-files'), // 临时文件自动删除时间,默认 5 分钟 cleanTimeout: 5 * 60 * 1000, // 原始 body 是否为 base64,默认 false,一般用于兼容腾讯云 base64: false, // 仅当路径命中 /api/upload 时才解析文件信息 match: /\/api\/upload/, }, };默认扩展名白名单(uploadWhiteList,可从@midwayjs/upload包导出)包含:.jpg、.jpeg、.png、.gif、.bmp、.wbmp、.webp、.tif、.tiff、.psd、.svg、.js、.jsx、.json、.css、.less、.html、.htm、.xml、.pdf、.zip、.gz、.tgz、.gzip、.mp3、.mp4、.avi(完整列表见 packages/upload/src/constants.ts)。
九、两种上传模式:file 与 stream
9.1 file 模式(默认,推荐)
data为上传文件在服务器上的临时文件地址,之后可用fs.createReadStream等方法读取内容;- 支持同时上传多个文件,多个文件以数组形式存放在
ctx.files中; - 因为会在服务器临时目录落盘,使用后应注意清理。
9.2 stream 模式
data为ReadStream,可通过pipe等方式把数据流转发到其他WriteStream或TransformStream;- 一次只上传一个文件(
ctx.files数组中只有一个文件对象); - 不会在服务器生成临时文件,取到内容后无需手动清理缓存。
从源码看,两种模式的分流点位于 packages/upload/src/middleware.ts:mode === 'file'时文件被写入tmpdir下以upload_${Date.now()}.${Math.random()}为前缀、扩展名取自白名单的随机临时文件;mode === 'stream'时则把二进制数据包装为Readable对象返回。
注意:部分函数计算平台不支持流式请求响应,
stream模式的实际可用性需参考对应平台能力说明。
十、安全加固:白名单与 MIME 校验
10.1 扩展名白名单
通过whitelist配置允许上传的扩展名;配置为null将跳过扩展名校验。安全提示:若设为null且使用file模式,攻击者可能上传.php、.asp等后缀的 WebShell 实施攻击。当然,由于组件会用随机生成的文件名落盘(upload_时间戳.随机数.扩展名),只要开发者不把临时文件地址返回给用户,风险相对可控,但仍不建议关闭校验。
另外,为防止恶意用户利用可被截断的扩展名绕过过滤,组件在解析扩展名时会过滤二进制数据,只保留0x2e(英文点.)、0x30-0x39(数字0-9)、0x61-0x7a(小写字母a-z)等字符,其他字符自动忽略(实现见 packages/upload/src/utils.ts 的formatExt)。
自 v3.14.0 起,whitelist还可以传入函数,根据请求上下文动态返回白名单:
// src/config/config.default.ts import { uploadWhiteList } from '@midwayjs/upload'; export default { // ... upload: { whitelist: (ctx) => { if (ctx.path === '/') { return ['.jpg', '.jpeg']; } else { return ['.jpg']; } }, // ... }, };10.2 MIME 类型校验(mimeTypeWhiteList)
攻击者可能把 WebShell 改名为.jpg绕过扩展名白名单,在某些服务器环境下仍被当作脚本执行。为此组件提供mimeTypeWhiteList配置,注意该参数默认没有值,即默认不做 MIME 校验。规则形如"扩展名 → MIME(可多个)":
// src/config/config.default.ts import { uploadWhiteList } from '@midwayjs/upload'; export default { // ... upload: { // 扩展名白名单 whitelist: uploadWhiteList, // 仅允许以下文件类型上传 mimeTypeWhiteList: { '.jpg': 'image/jpeg', // 可配置多个 MIME,例如 .jpeg 允许 jpg 或 png '.jpeg': ['image/jpeg', 'image/png'], '.gif': 'image/gif', '.bmp': 'image/bmp', '.wbmp': 'image/vnd.wap.wbmp', '.webp': 'image/webp', }, }, };也可以直接复用组件导出的DefaultUploadFileMimeType作为默认 MIME 校验规则,它提供了.jpg、.png、.psd等常用扩展名的 MIME 映射(见 packages/upload/src/constants.ts):
import { uploadWhiteList, DefaultUploadFileMimeType } from '@midwayjs/upload'; export default { upload: { whitelist: uploadWhiteList, mimeTypeWhiteList: DefaultUploadFileMimeType, }, };MIME 识别依赖file-type包(当前仓库锁定版本21.3.4,见 packages/upload/package.json),其支持的文件类型范围请以该包文档为准。两点提示:
- MIME 校验规则仅适用于
mode=file模式; - 设置后需要读取文件内容进行匹配,上传性能会略有影响,但从安全角度仍建议尽量开启。
自 v3.14.0 起,mimeTypeWhiteList同样支持函数式动态返回:
export default { upload: { mimeTypeWhiteList: (ctx) => { if (ctx.path === '/') { return { '.jpg': 'image/jpeg' }; } else { return { '.jpeg': ['image/jpeg', 'image/png'] }; } }, }, };10.3 限定上传路径:match/ignore
组件启用后,任何POST/PUT/DELETE/PATCH请求只要content-type是multipart/form-data且带boundary,就会自动进入上传解析逻辑,在临时目录创建文件缓存。这意味着恶意用户可以手动构造请求,向任意普通接口上传文件,导致服务器负载升高、缓存占满。
因此强烈建议通过match或ignore配置限定允许解析上传的路径:
upload: { // 仅当路径命中 /api/upload 时才解析文件 match: /\/api\/upload/, // 或者反向排除:ignore: [/^\/public\//], }从 packages/upload/src/middleware.ts 可以看到,match与ignore是互斥的:配置了match则只处理匹配路径,否则才使用ignore排除路径。
十一、临时文件与清理
使用file模式时,上传文件会保存在tmpdir指向的目录中,可以通过两种方式清理:
- 自动清理:
cleanTimeout控制自动清理间隔,默认5 * 60 * 1000(5 分钟),设为0可关闭自动清理。底层由 packages/upload/src/utils.ts 的autoRemoveUploadTmpFile定时扫描临时目录,删除创建时间(ctimeMs)早于cleanTimeout的文件。 - 主动清理:在代码中调用
await ctx.cleanupRequestFiles(),删除当前请求产生的全部临时文件。
组件在onStop阶段会调用stopAutoRemoveUploadTmpFile停止定时清理任务(见 packages/upload/src/configuration.ts),避免进程退出时残留定时器。
十二、安全警示清单
结合组件源码与官方文档,启用上传功能后请自查以下三点:
- 扩展名白名单:
whitelist是否开启?设为null时可能被用于上传.php、.asp等 WebShell。 - 路径限制:是否配置了
match或ignore?否则普通POST/PUT接口可能被攻击者利用,导致服务器负载与磁盘占用上升。 - 文件类型校验:是否配置
mimeTypeWhiteList?否则攻击者可能伪造文件类型绕过扩展名白名单。
十三、测试验证
仓库为上传组件提供了完整的测试覆盖(见 packages/upload/test):koa.test.ts、express.test.ts、web.test.ts、faas.test.ts分别覆盖不同框架下stream与file两种模式;koa.test.ts中还包含 MIME 校验、whitelist设为null、函数式白名单、allowFieldsDuplication重名字段等场景;clean.test.ts则验证临时文件自动清理逻辑。如果你修改了上传相关配置,可参考这些用例验证行为是否符合预期。
结语
通过@midwayjs/hooks-upload与@midwayjs/upload的组合,Midway Hooks 让文件上传从"处理ctx、解析multipart、管理临时文件"的繁琐流程,收敛为一行Upload('/api/upload')加一个useFiles()的纯函数接口。配合扩展名白名单、MIME 内容校验、match/ignore路径限制与自动清理机制,即可在获得开发效率的同时守住安全底线。更多配置细节可继续阅读仓库内的 hooks/upload.md 与 extensions/upload.md。
- 后端
- 微服务
- 云原生
【免费下载链接】midway
🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈
相关推荐
Midway Hooks 一体化文件上传实战:@midwayjs/hooks-upload 接口声明、前端调用与源码原理
Midway Hooks 一体化文件上传实战:@midwayjs/hooks upload 接口声明、前端调用与源码原理 导读 在 Midway Hooks 的
后端微服务云原生Midway 通用文件上传组件 @midwayjs/upload 实战指南:file / stream 双模式与安全配置
Midway 通用文件上传组件 @midwayjs/upload 实战指南:file / stream 双模式与安全配置 文件上传是 Web 应用与服务端接口最
后端微服务云原生Midway 文件上传组件实战:@midwayjs/upload 的 file/stream 双模式、白名单校验与临时文件清理
Midway 文件上传组件实战:@midwayjs/upload 的 file/stream 双模式、白名单校验与临时文件清理 @midwayjs/upload
后端微服务云原生
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考