1. 项目概述与方案选型
1.1 核心需求解析
这个"121 文件上传云存储"项目,起因其实特别朴素。去年我接手了一个内部运营管理系统,里面有一个模块是做单据台账的电子化管理,前期一直图省事,文件直接往应用服务器的本地磁盘目录里扔。用户上传的预算表、审批单、合同扫描件,还有每月批量导入的Excel,全部堆在/data/upload这个路径下面。
随着业务跑起来,问题很快就来了:磁盘分区报警、备份脚本要额外打包这台机器的目录、多实例部署时代码里写死的物理路径根本没法共享。最尴尬的是,有一次发布新版本,负责运维的同事没看清脚本,直接把旧目录清掉了。那天的场景我到现在都记得——用户侧所有历史附件全部白屏,我对着备份介质翻了两个小时才把数据捞回来。
所以"121"这个项目,本质上做的一件事就是:把文件从"应用服务器的本地磁盘"迁移到"独立的云存储服务",通过一套统一的上传接口对外提供服务。不管底层是自建对象存储还是云厂商OSS,对业务方来说只需要拿到一个上传凭证,就能把文件可靠地传上去,拿到一个可访问的URL落库即可。
这个方案适合谁参考?我觉得是三类人:一是中小团队想给存量项目加文件存储能力但不想动业务表的;二是后端工程师想搞明白"前端直传+签名URL"这套标准玩法到底怎么落地的;三是被"文件上传漏洞"这个词吓到过、想知道怎么把上传接口做安全的同学。这篇文章我会把我们完整的选型思路、接口设计、安全防护踩坑、以及部署排障过程都拆开讲一遍。
1.2 为什么放弃本地磁盘,转向对象存储
先算一笔账。我们的应用服务器是4C8G的云主机,数据盘给了100G。业务上线初期每天上传量大概在200MB左右,看起来不多,但半年之后就发现几个隐藏问题:
第一,存储和计算耦合。文件写入占用的IOPS和带宽,会直接影响应用自身的接口响应。尤其是月底批量导入的时候,有几张Excel动辄几十MB,一传进来,同台机器上其他接口的延迟肉眼可见地往上飙。
第二,扩容和备份都很痛苦。云主机数据盘扩容不是不能做,但要么停机操作,要么在线扩容后还要进系统调整分区,运维每一步都得小心翼翼。备份更是麻烦,逻辑备份工具基本不管文件,只能靠shell脚本打包rsync,一旦并发上传的文件正在写入,打包出来的副本可能是损坏的。
第三,多实例部署时文件不一致。我们后来把应用扩到了两台实例,负载均衡分发请求,结果用户这次上传的附件落到A机器,下次访问被路由到B机器,直接404。临时方案是挂NFS共享目录,但NFS的单点故障和并发性能又成了新问题。
对象存储把这些问题一次性都解决了。文件独立存储、独立扩缩容,应用实例只管业务逻辑;备份可以走存储桶级别的策略,比如MinIO自带版本控制和异地同步;文件访问走HTTP协议,天然支持多实例共享。我们当时的结论是:本地磁盘适合存临时文件和配置,业务产生的持久化文件必须交给专门的存储组件。
1.3 技术选型:自建MinIO还是直接用云厂商OSS
说到云存储,国内团队的第一反应往往是阿里云OSS、腾讯云COS。我们一开始也是这么想的,毕竟云厂商的服务开箱即用,控制台界面也友好。但对比之后发现几个让我们犹豫的点:
- 云厂商OSS按量计费,流量费+请求费+存储费叠加,我们的文件类型大量是图片和PDF,会被频繁预览下载,一个月的公网流量费估算下来不是小数目。
- 有些数据涉及内部敏感信息,虽然用云厂商也没问题,但合规评审时多一道解释成本。
- 我们的服务器本来就在内网互通的环境里,自建对象存储走内网访问,延迟和带宽都比走公网再回源要稳。
于是我们把目光转向自建方案。对象存储的选型其实不少,比如MinIO、SeaweedFS、Ceph RGW、FastDFS,还有老牌的GlusterFS。我们的筛选逻辑很简单:
- Ceph RGW功能强大但运维门槛太高,一个生产集群至少三台起,不适合我们这个规模。
- FastDFS早年很流行,但社区活跃度和S3协议兼容性都不如新生代项目。
- MinIO使用Go语言开发,部署就是一个二进制或一个Docker容器,兼容AWS S3 API,生态成熟,文档齐全,而且支持单机模式跑起来做开发测试。
最后我们定了MinIO。另外说句题外话,如果团队连独立服务器都不想为存储这事维护,直接用云厂商OSS的预设签名URL功能完全可行,下文讲的签名直传思路在阿里云、腾讯云上也是一模一样的玩法。
1.4 架构方案:服务端签名 + 客户端直传
定了存储组件,接下来要决定文件上传的链路。这里有两套常见方案:
方案A:上传文件先进应用服务器,再转发到对象存储。这种方案代码写起来最简单,前端POST到后端接口,后端用SDK转存到MinIO。但坏处很明显:文件要经过应用服务器中转,带宽和磁盘IO都白白消耗一遍;大文件上传时后端连接长时间被占用,还得自己控制超时。
方案B:应用服务器只负责下发签名URL,前端拿到URL之后直接PUT到对象存储。这种方案是业界标准玩法。MinIO支持预签名URL(Presigned URL),可以生成一个指定时间内有效的上传地址,只有持有该URL的客户端才能往这个bucket里写对象。文件流完全不经过应用服务器,后端只做一次签名计算和元数据登记。
我选的是方案B。理由很简单:我们的文件平均大小在5MB左右,最大有50MB的Excel,方案B能把上传压力全部卸载到MinIO上,应用服务器对文件传输几乎零感知。签名URL的过期时间我们设置在10分钟,足够用户完成一次上传,又不会让URL在网络上长期有效形成安全隐患。
整体架构用一句话描述就是:前端发请求换取上传凭证 → 后端生成MinIO签名URL → 前端PUT直传 → 文件落地后回调后端登记元数据。
2. 核心功能拆解与实现细节
2.1 后端签名接口设计
我们的上传凭证接口路径是/api/v1/file/presign,请求参数只有三个:fileName(文件名)、fileSize(文件大小)、bizType(业务类型,比如contract、invoice、import)。后端拿到这些参数后,会做两件事:第一件是校验,第二件是生成签名URL。
校验逻辑看起来简单,但很关键。文件名要过滤掉路径分隔符和非法字符,防止用户传../../etc/passwd这种恶意构造的名称;业务类型必须在前端枚举列表里,防止有人绕过页面任意指定bucket路径;文件大小要同时符合单文件上限要求,我们的上限设置为100MB,超了直接返回413。
校验通过后,生成一个随机的对象名。这里有个很重要的点:绝不能直接用用户上传的原始文件名作为对象名。一方面是因为原始文件名可能包含中文、空格、特殊符号,URL编码后非常丑;另一方面是如果两个用户上传了同名文件,后面一个会把前面一个覆盖掉。
我们采用bizType/yyyyMMdd/UUID.扩展名的结构,扩展名从原始文件名中提取,UUID保证全局唯一。对应代码如下:
import uuid from datetime import datetime from minio import Minio client = Minio( endpoint="minio.internal:9000", access_key="your-access-key", secret_key="your-secret-key", secure=False ) def generate_upload_url(file_name: str, file_size: int, biz_type: str) -> dict: ext = file_name.rsplit(".", 1)[-1] if "." in file_name else "bin" object_name = f"{biz_type}/{datetime.now():%Y%m%d}/{uuid.uuid4().hex}.{ext}" url = client.presigned_put_object( bucket_name="app-files", object_name=object_name, expires=timedelta(minutes=10) ) return { "upload_url": url, "object_name": object_name, "download_url": f"/api/v1/file/access/{object_name}", "expires_in": 600 }签名URL的过期时间为什么设置在10分钟?这是我们考虑过的:太短的话用户可能因为网络慢还没传完就过期了,太长的签名URL等于在网络上留了一个上传接口,给恶意者更多扫描利用的时间窗口。10分钟对于正常用户的文件选择、上传操作是足够的,实际测试中99%的上传都在5分钟内完成。
2.2 前端分片与进度展示
虽然后端不参与文件流,但前端上传的体验控制还是要做到位。现代浏览器原生XMLHttpRequest支持上传进度事件,至少要让用户看到一个明确的进度条,而不是干等着。我们封装了一个uploadToMinIO(url, file, onProgress)的函数,本质上是把xhr.upload.onprogress暴露给UI层。
对于小文件(小于50MB),直接用一次PUT请求就能搞定,简单可靠。对于超大文件或者网络不稳定的场景,MinIO也支持基于S3的Multipart Upload,也就是分片上传。分片的好处是:某一小片失败了只需要重传那一片,不需要重启整个文件;而且可以并发上传多片,充分利用带宽。
我们在121项目里的分片策略是这样的:文件大于50MB时,按7MB一片切片(50MB除以7约等于8片,并发3片上传),每片用UploadPart接口独立上传,全部完成后用CompleteMultipartUpload合并。前端要记录每片的ETag和Part Number,这也是分片上传最烦琐的地方。
const PART_SIZE = 7 * 1024 * 1024; const CONCURRENCY = 3; async function uploadInParts(file, uploadUrl) { const totalParts = Math.ceil(file.size / PART_SIZE); const pool = []; for (let i = 1; i <= totalParts; i++) { pool.push(uploadPart(file, i, PART_SIZE, uploadUrl)); if (pool.length >= CONCURRENCY) { await Promise.race(pool); // 并发控制 } } await Promise.all(pool); }当然,直接使用MinIO的JS SDK可以省去很多底层细节,比如minio-js的putObject方法内部会自动处理分片。但公司内部有安全要求,不希望页面里直接集成带accessKey的SDK,所以我们的前端是纯白用签名URL的裸PUT方式,分片逻辑只能自己写。如果你所在团队没有这个限制,直接用官方SDK会省心很多。
进度条展示有个小坑:分片并发时,如果只算"已上传分片数/总分片数",进度会跳变得很生硬;我们把每一片的已上传字节数累加起来除以总字节数,进度就平滑了。同时要处理网络错误,单片失败重试最多3次,超过3次就终止上传并提示用户检查网络。
2.3 秒传与断点续传的实现方案
用户经常抱怨总在重复上传同一个文件,比如同一个月的预算表改了两次,文件名内容都没变。为了优化体验,我们做了一个简易的秒传机制:前端在上传前先计算文件的MD5哈希,把MD5和文件大小带给后端查询。
后端在数据库里建了一张file_metadata表,记录每个文件的MD5和对象名。如果MD5完全匹配,后端直接返回已有文件的下载地址,前端弹一个"文件已存在,直接使用"的提示,一秒完成上传。
这个机制要注意的一个边界问题是:用MD5判断文件相同不是100%可靠的,虽然碰撞概率极低,但对于极其重要的合同文件,建议在后端还是保留一个校验步骤,确保文件大小完全相同再判定为同一个文件。我们实际判断条件是两个字段的联合匹配:md5 + file_size。
断点续传我们没有做。原因很现实:首先,断点续传需要记录每个分片的上传状态,前端刷新后状态丢失,要么存在IndexedDB,要么后端维护一个UploadSession表,复杂度上升不少;其次,我们的文件最大只是100MB,重传一次的成本完全可接受。如果你要传几GB的日志包,那断点续传是刚需,但配合签名直传模式,断点续传需要先调用MinIO的CreateMultipartUpload获取UploadId,再把UploadId返回给前端,这个流程我们初期评估过,投入产出比不划算。
2.4 业务表结构与元数据管理
文件上传完成后,前端会拿着文件的objectName和ETag,再去调一次业务接口告诉后端"这个文件传完了"。后端正是在这一步把元数据写入数据库。
file_metadata表结构很简单,核心字段包括:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint PK | 自增主键 |
| biz_type | varchar(32) | 业务类型 |
| object_name | varchar(255) | MinIO桶内对象名 |
| original_name | varchar(255) | 用户上传时的原始文件名 |
| file_size | bigint | 文件大小(字节) |
| md5 | varchar(32) | 文件MD5值 |
| etag | varchar(64) | MinIO返回的ETag |
| uploader_id | varchar(64) | 上传人用户ID |
| created_at | datetime | 上传时间 |
为什么要把object_name和original_name分开存?因为下载时要给用户提供带原始文件名的下载响应,而存储时用的是平台侧生成的UUID名,两个名字职责不同。这也是我反复跟团队成员强调的:用户看到的文件名永远应该是original_name,不能暴露内部的对象名规则。
访问文件时我们走一个/api/v1/file/access/{objectName}的后端代理接口,后端查表拿到original_name,再去MinIO生成一个带短时效的GET签名URL(默认5分钟有效),然后下发302重定向。这里没有用公有读策略,原因是公网桶如果设置成任何人都可读,等于把用户上传的合同和身份证照片裸奔在公网上,安全审计时必然被怼。
3. 文件上传安全防护实践
3.1 为什么文件上传接口是攻击重灾区
业内聊到Web安全,文件上传功能绝对是高频漏洞入口。热搜词里那些"任意文件上传""文件上传绕过""MIME类型修改"指向的几乎是同一类问题:攻击者想方设法让服务器接受一个本不该被接受的可执行文件。
这个问题的严重性在于,文件上传一旦被绕过,攻击者上传的可能是一个WebShell脚本。如果服务器把上传目录映射成了可执行的Web路径,攻击者访问这个文件就等于拿到了服务器的远程控制权。OWASP ZAP这类安全测试工具扫描一个站点时,上传接口是必测的点位,它会尝试上传各种畸形后缀、双扩展名、编码绕过载荷,看服务器是否照单全收。
所以我把安全防护看作是整个121项目中优先级最高的一环,宁可功能少做一点,上传校验的每一道关卡都绝不能省。下面说我们实际做的几层防护。
3.2 前端MIME限制只是体验,后端必须强制校验
很多团队在上传功能里只做前端校验,比如在input标签里加accept="image/png,image/jpeg",或者在上传前用JS检查file.type。这些手段防君子不防小人,因为攻击者完全可以用curl、Postman或BurpSuite构造请求包,前端的任何限制都拦不住他们。
但MIME类型校验也不是完全没意义,它对正常用户能起到很好的引导作用。我们前端限制的是可选文件类型,同时在页面提示里写明格式要求。真正的安全拦截必须放在后端。
后端第一道校验是检查HTTP请求的Content-Type头。注意,这个字段同样可以被伪造,所以只能作为第一层粗筛。更可靠的方案是把"扩展名校验"和"文件内容校验"结合起来。
3.3 扩展名白名单与文件头魔数校验
扩展名校验的核心原则是:永远用白名单,不用黑名单。黑名单思路是拦截"php、jsp、aspx、exe"等危险后缀,但攻击者总能找到你没想到的变种,比如php5、phtml、pht、shtml、jspx等,黑名单永远追不上。
白名单思路是只允许业务确实需要的类型。我们的业务场景是图片、PDF、Excel文档,那白名单就明确写成:
jpg, jpeg, png, gif, bmp, webp, pdf, xls, xlsx, csv, doc, docx不在白名单里的后缀,一律拒绝,不管它是有意还是无意。这个方法简单粗暴,但效果最好。
扩展名校验通过后,还要校验文件内容。之所以要这么做,是因为攻击者可以把一个PHP脚本命名为1.jpg上传,如果服务器只是看后缀就放行,那这个伪装成图片的脚本就被存下来了。文件内容的校验方式有两种:一种是解析文件头(魔数),另一种是用专业的解析库。
文件头魔数是最经济的方案。PNG文件的前8个字节固定是89 50 4E 47 0D 0A 1A 0A,JPEG文件前3个字节是FF D8 FF,PDF文件以25 50 44 46开头(即%PDF)。在MinIO的接收端校验,我们可以先让前端上传完成后,后端从MinIO下载文件的前2KB,用Python的python-magic库区识别真实类型,再和允许的类型比对。
import magic def verify_file_content(object_name: str) -> bool: # 模拟从MinIO读取前2KB file_bytes = read_file_head(object_name, size=2048) detected_type = magic.from_buffer(file_bytes, mime=True) allowed_mime = { "image/jpeg", "image/png", "image/gif", "application/pdf", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" } return detected_type in allowed_mime这个方案比纯看扩展名安全得多,但也要注意一个体验问题:图片的伪装文件可以通过颜色配置文件等方式让魔数识别失败。另外,WPS生成的xls文件,mime类型可能是application/octet-stream,这类边界情况要及时在白名单里放行,否则会被用户骂惨。
3.4 常见绕过方式与对应防护
安全测试反馈过来的绕过手法,我把它们整理成一张表,每个都对应了我们实际加的防线:
| 绕过方式 | 攻击原理 | 我们的防护措施 |
|---|---|---|
| 双扩展名 | 上传shell.php.jpg,某些服务器解析时看最后一个扩展名 | 扩展名必须完全匹配白名单,从最后一个.截取后整体判断,不允许中间出现可执行后缀 |
| 大小写变种 | 上传shell.PhP或shell.pHp | 扩展名统一转为小写后校验 |
| 空字节截断 | 在早期版本中shell.php%00.jpg会被截断为shell.php | 现代Web服务器已修复此问题,但我们依然做了过滤,发现%00直接拒绝 |
| 文件头伪造 | 在PHP脚本前加上GIF89a文件头,让魔数验证通过 | 用专业解析库解析文件结构,而不只看前几字节;PHP脚本即便MIME伪装,结构上仍能被识别为text/plain |
| 修改Content-Type | 直接用curl把请求的Content-Type改为image/jpeg | 后端不信任该字段,仅作提示;最终以文件头和扩展名双重校验为准 |
| 恶意文件名 | 文件名包含../、..\\等路径穿越字符 | 后端统一使用UUID重命名对象,原始文件名只保存在数据库展示字段中 |
| 文件覆盖 | 通过已有对象名直接上传,覆盖历史文件 | 对象名由后端生成UUID,且每次生成随机不可预测,旧文件不可能被用户猜到 |
最后特别提一下"文件覆盖"。这个攻击向量很多团队会忽略。如果你使用对象存储时,对象名是用户可控的,比如直接取了原始文件名report.pdf,那攻击者只需要构造一个恶意PDF,用同一个文件名覆盖掉业务方已经上传的合同附件,就能实现投毒。我们的UUID随机生成策略,从根本上杜绝了这种覆盖路径。
3.5 用OWASP ZAP对上传接口做主动扫描
项目上线前,我们用OWASP ZAP对整个系统做了一轮主动扫描,重点就是文件上传相关路径。ZAP有一个"Fuzzer"插件,可以针对上传请求做载荷变异测试,跑出了不少之前没考虑到的问题。
这里分享几个真正的扫描发现,以及对应的修复过程:
第一个发现是上传接口没有做CSRF防护。ZAP构造了一个恶意页面,自动提交一个表单往/api/v1/file/presign发请求,如果此时用户处于登录状态,攻击者就能诱导用户浏览器帮自己创建上传凭证。修复方式是在后端统一拦截器里校验CORS配置,并且要求请求必须携带自定义Header(比如X-Requested-With: XMLHttpRequest),跨域恶意表单无法带这个头。
第二个发现是签名URL的生成逻辑使用了错误的HTTP方法。最初我们给MinIO SDK传了method='POST',但前端实际用的是PUT,导致部分文件能传、部分文件报405。ZAP扫描时给同样的URL发送不同方法,结果暴露出配置不一致。修复后统一为PUT,并在签名时明确指定方法,避免方法混淆被利用。
第三个发现是对超大并发上传没有限流。ZAP做了20个并发请求,我们的presign接口响应从50ms飙到3秒,虽然没打崩,但明显有性能隐患。我们在网关层加了一个简单的令牌桶限流,按IP限制每分钟60次上传凭证请求。正常用户根本不会触发这个阈值,但恶意扫描器会被快速拦截。
4. 部署配置与问题排查实录
4.1 MinIO部署与基础配置
MinIO的部署方式我们选的是Docker Compose,这是中小团队最省心的方案。docker-compose.yml配置如下:
version: '3.8' services: minio: image: minio/minio:RELEASE.2024-01-16T16-07-38Z container_name: minio restart: always command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: admin MINIO_ROOT_PASSWORD: Strong-Password-Change-Me volumes: - /data/minio:/data ports: - "9000:9000" - "9001:9001" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 10s retries: 3有几个点必须强调:第一,MINIO_ROOT_PASSWORD至少16位且混合大小写和特殊字符,这个账号相当于整个对象存储的超级管理员,泄露等于数据全部暴露;第二,数据目录建议挂载到独立数据盘,不要和系统盘混在一起;第三,9001是Web控制台端口,只在内部网络开放,不要映射到公网。
MinIO启动后用浏览器访问http://服务器IP:9001,使用配置的账号登录。第一步是创建一个专用bucket,我们命名为app-files。创建时建议关闭桶的公共读写权限,默认Private即可。第二步是在"Access Keys"页面创建一个新的Access Key,这个Key只授权给应用服务使用,不要给前端或客户端使用。
4.2 Nginx反向代理与HTTPS
MinIO虽然可以直接对外提供服务,但我们没有让它直接暴露在业务流量入口,而是放在Nginx后面做反向代理。原因有两个:一是统一SSL证书管理,二是在Nginx层可以做请求体大小限制和访问日志。
server { listen 443 ssl; server_name minio.example.com; ssl_certificate /etc/nginx/ssl/minio.crt; ssl_certificate_key /etc/nginx/ssl/minio.key; client_max_body_size 200m; location / { proxy_pass http://127.0.0.1:9000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_pass_header etag; proxy_request_buffering off; } }注意proxy_request_buffering off这一行很关键。Nginx默认会把请求体缓冲到本地磁盘再转发给后端,对大文件上传来说,等于让Nginx又当了一次中转站,磁盘IO和延迟都会增加。关闭后Nginx直接流式转发请求体给MinIO,性能表现好很多。
HTTPS证书我们用的是内部证书管理系统签发的,有效期一年。如果你不配置HTTPS,浏览器里发起的PUT请求会被当作混合内容拦截,前端直传方案根本跑不起来。这一点在部署文档里我特意用醒目标注标出来了。
4.3 常见问题排查速查表
项目上线后集中遇到了一些问题,这里整理成一张速查表,方便大家对照排查:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 前端上传报CORS错误 | MinIO桶的CORS配置未允许目标域名 | 在MinIO控制台或通过mc命令添加CORS规则,允许来源域名和PUT方法 |
| 上传超过100MB被Nginx拒绝 | Nginx的client_max_body_size没调 | 修改为需要的值,比如200m,并重载Nginx |
| 下载文件名中文乱码 | URL编码处理不当 | 下载接口设置Content-Disposition头时使用filename*=UTF-8''格式 |
| 图片无法直接在浏览器预览 | bucket访问策略限制 | 不能简单设为公有读;应使用签名URL或后端代理访问 |
| 签名URL过期时间不好控制 | 客户端系统时间不准 | 确保浏览器时间正确;后端签名基于服务器时间,客户端偏差大会出现访问提前失败 |
| MinIO磁盘写满 | 未设置桶配额或生命周期策略 | 在MinIO控制台为桶设置配额,并配置生命周期规则,按天清理临时目录 |
4.4 一个真实的线上故障复盘
上线第二个月,我们遇到过一个匪夷所思的问题:某个用户上传Excel后,反馈文件打不开。排查发现该文件并不是在121项目的上传页面传的,而是用户从上一个旧系统的本地目录直接拷贝到新系统的上传目录,导致文件实际是损坏的。
这个案例给我的启示是:文件上传安全校验固然重要,但"完整性校验"同样不能忽略。我们后来在签名接口中增加了一个可选参数expected_md5,前端在上传前计算出文件MD5传给后端,后端在签名URL中带上该MD5,MinIO在接收完文件后会自动比对,如果不一致则拒绝落盘。这样从入口就拦截了损坏文件,避免了用户下载到坏文件时的困惑。
另外,在下载侧我们给file_metadata表增加了download_count字段,每次生成下载链接时记录日志,方便审计谁在什么时间下载了哪个文件。这个记录在安全复盘时会变成重要的溯源依据。
5. 一点实操心得
这套文件上传云存储的改造,从需求评审到上线用了大概两周,真正开发时间只有五天。回头看我感触最深的一点是:对象存储迁移并不难,难的是把"上传链路"和"安全边界"一起想清楚。
如果你正准备做类似改造,我个人建议按这个顺序推进:先用docker把MinIO跑起来,写一个最小可用的presign接口,前端用Postman或curl验证直传链路跑通,再回头补安全校验、CORS、HTTPS、限流这些外围能力。不要把摊子铺太大,先让核心链路动起来,每一步都能看到效果,后面碰到问题也有明确的排查方向。
最后再分享一个小技巧:给MinIO的HealthCheck加上告警,写一个简单的shell脚本每分钟检查9000端口和磁盘使用率,超过80%自动钉钉通知。存储这玩意儿平时没存在感,真出故障就是大事,早发现五分钟可能就少一次数据迁移事故。