“一个月300块?就存点头像图片?”
我盯着阿里云OSS的测试账单,手抖得比咖啡因过量还厉害。这还只是测试期的费用,真上线了还得了?
“不行,”我咬牙,“这违背开源精神。”
作为一个开源项目的维护者,我有个固执的原则:绝不让用户为开源项目花一分钱。OSS?拜拜了您嘞。
周一,我做出了那个改变一切的决定:从OSS迁移到本地存储。
“简单,”我想,“Docker + Nginx,文件存本地,零成本。”
周二,我开始改造。后端Java代码好办,但前端上传怎么办?我瞄了一眼Vue组件——一个头像上传组件就400多行代码,各种校验、预览、裁剪。
“让trae帮我弄吧。”我偷了个懒。
我对IDE里的trae说:“帮我改一下头像上传,不用OSS了,存本地Nginx。”
trae秒回方案:“收到。使用Vite反向代理,前端直接上传到/uploads目录,Nginx直接服务。”
代码很简单:
javascript// 上传
POST /uploads
// 前端直接处理,不走Java
我测试了一下,速度飞快。Node.js处理文件上传,确实比Java省资源。
但周三,我发现一个问题:用户上传新头像,旧头像还在服务器上躺着!
“这不行,”我的资源洁癖发作了,“如果有人恶意上传,服务器不就炸了?”
我命令trae:“加删除逻辑。用户上传新头像前,先删除旧头像。”
trae很听话,把单次上传改成了两个操作:
DELETE /uploads/旧文件)POST /uploads)“完美。”我当时想。
周四晚上,预上线前的最后一次安全测试。
我随手在APIFox里试了一下:
DELETE http://test.zymusic.top/uploads/admin.png
200 OK。
我又试了试:
POST http://test.zymusic.top/uploads (随便传个文件)
200 OK。
我的血液凝固了。
这两个接口完全没有权限验证!任何人,不需要登录,就能删文件、传文件!
“我草!trae你……”我骂到一半,突然意识到:trae只是个AI,它懂个屁的权限验证。
凌晨2点,我盯着架构图,发现了几个致命矛盾:
矛盾一:性能 vs 安全
矛盾二:资源洁癖 vs 安全风险
矛盾三:开源理想 vs 工程现实
凌晨3点,我必须在几个小时内解决这个问题,否则周一预上线就是个笑话。
方案一:让前端直接问Java权限(不行) 因为最终文件操作还是要和Nginx交互,JavaScript处理IO比Java高效得多。而且,让JS解析JWT?不可接受!JWT是Java生成的,就该Java来解析。
方案二:让Java直接管理文件(不行) 性能下降不说,还要大改前后端,一周时间根本不够。
方案三:在Node.js和Nginx之间加一层验证(可行!)
我的方案渐渐清晰:
但还有个问题:如何确保用户是“先删后传”,而不是只传不删,或者只删不传?
我的解决方案:用Redis记录操作状态。
javascript// Node.js上传服务伪代码
// 删除接口
app.delete('/uploads/:filename', async (req, res) => {
// 1. 从header拿到JWT
const token = req.headers.authorization;
// 2. 问Java:这人有权限删这个文件吗?
const canDelete = await javaBackend.verifyDelete(token, filename);
if (!canDelete) {
return res.status(403).send('滚犊子');
}
// 3. 执行删除
await fs.unlink(filepath);
// 4. 在Redis标记:这个用户已删除旧头像,可以上传新的了
await redis.setex(`upload_ok:${userId}`, 5, '1');
res.send('删除成功');
});
// 上传接口
app.post('/uploads', async (req, res) => {
const token = req.headers.authorization;
// 1. 问Java:这人有权限上传吗?
const canUpload = await javaBackend.verifyUpload(token);
if (!canUpload) {
return res.status(403).send('没权限');
}
// 2. 检查Redis:他是不是刚删了旧头像?
const canProceed = await redis.get(`upload_ok:${userId}`);
if (!canProceed) {
return res.status(403).send('请先删除旧头像');
}
// 3. 执行上传
const newFilename = await saveFile(req.file);
// 4. 清理Redis标记
await redis.del(`upload_ok:${userId}`);
res.send({filename: newFilename});
});
Java后端的验证逻辑:
java// Java伪代码
public boolean verifyDelete(String token, String filename) {
// 1. 解析JWT,获取用户ID
String userId = decodeJWT(token);
// 2. 查数据库,这个用户的当前头像是不是这个文件?
User user = userDao.findById(userId);
if (user.getAvatar().equals(filename)) {
return true; // 是自己的头像,可以删
}
return false; // 不是自己的头像,滚
}
public boolean verifyUpload(String token) {
// 管理员?直接放行!
if (isAdmin(token)) {
return true;
}
// 普通用户:检查是否有上传权限(比如是不是被封禁了)
return userCanUpload(token);
}
凌晨4点,我部署完这个“四不像”的架构:
前端 → Node.js上传服务 → Java权限验证 → Redis状态管理 → Nginx文件服务
绕了地球一圈,但:
“这就是工程现实吗?”我苦笑,“为了不花钱,我造了个比OSS复杂十倍的轮子。”
系统终于安全了。我瘫在椅子上,回顾这一周的过山车:
我以为“不花钱”就是开源精神,但忽略了“不花钱”可能意味着“花更多精力”。OSS的300块,买的是别人解决好的安全方案。我省了300块,搭进去一周的睡眠。
我的担心是对的——恶意上传真的能搞垮服务器。但我的解决方案错了——不应该为了省资源而牺牲安全。应该先保证安全,再考虑资源。
trae能写出漂亮的代码,但不懂架构、不懂安全、不懂权衡。它能执行命令,但不能思考后果。把AI当工程师用,就像让厨子开飞机——早晚出事。
我既想要Node.js的IO性能,又想要Java的安全验证。鱼和熊掌不可兼得,但我用Redis和HTTP调用强行兼得了。代价是:复杂度爆炸。
一周前,我以为只是换个存储方案。一周后,我造了个分布式文件权限系统。有时候,工程进度不是按计划走的,是按问题走的。
如果你也在维护开源项目,想坚持“完全免费”,记住我的教训:
用户能接受功能少,但不能接受数据丢。安全漏洞是开源项目的死刑。
你省了云服务的钱,但付出了开发、维护、调试的精力。这些精力也是成本。
特别是安全测试。我要是早做安全测试,周二就能发现问题,不用拖到周四凌晨。
AI能帮你写代码,但不能帮你做架构决策。你是工程师,它是工具。别搞反了。
300块买OSS服务,还是300小时造轮子?在开源项目里,时间也是稀缺资源。
窗外天空泛白,一周的挣扎结束了。
我的开源项目依然坚持“完全免费”,但代价是:一个比商业方案复杂十倍的权限系统。
这值得吗?我不知道。
但我知道的是:当用户不用为这个项目花一分钱时,他们不会知道,有个傻子在深夜用Redis、Node.js、Java和Nginx,造了个丑陋但安全的轮子。
而那个傻子,现在需要睡一觉。
预上线倒计时:12小时。
这次,真的准备好了。🚀

