低成本自建阿里云函数计算FC+OSS图床

源码:https://github.com/ezzty/oss-upload
先说三件事:
- 本文所有
AccessKey、密码、域名、Bucket 名都是占位符,照抄前请替换成你自己的。 - 代码是单文件 Node.js(约 380 行),依赖通过 FC 层(Layer) 注入,业务代码零硬编码,全部配置走环境变量。
- 走完这篇,你会得到:
登录 → 拿 token → 上传 → 返回 Markdown 图片语法的完整链路,且上线前有可执行的验收清单。
一、为什么自建图床
每一个静态博客都必备第三方图床;但是哪个图床不跑路呢?很难说。而且你用别人的图床:防盗链规则不受你控制、服务跑路图片全 404、内容审核误伤、免费额度藏陷阱。
踩了很多坑之后,决定自己给自己搞一个极低使用成本的图床。主要目的就是省钱,不限量!
- 数据在自己账号里,随时能导出、能迁移
- 计费按量,个人图床的用量基本落在免费额度边缘
- 安全策略自己定:谁能传、传多大、放哪个目录、给谁看,全部可控
代价是你要自己处理鉴权、防爆破、类型校验这些脏活——本文已经替你处理完了。
二、架构与请求流程
浏览器 upload.html
│ ① POST /login {user, pass}
│ ← 200 {token}(有效期 24h)
│ ② POST /upload Authorization: Bearer + 图片二进制
▼
阿里云函数计算 FC(Node.js 18,HTTP 触发器)
│ 鉴权 / 限流 / 魔数校验 / 体积校验
│ ③ ossClient.put(key, stream)
▼
OSS Bucket(私有读写,禁止公共读)
│ ④ CDN 回源(内网,免费)
▼
https://img.example.com/m/26j13/ab12cd.jpg ← 对外访问域名
三个设计取舍:
- Bucket 设为私有:不给任何人直链读权限,图片只能通过绑定了 CDN 域名的 URL 访问。一旦公开读,你的桶就变成了公共资源,别人能列举、能白嫖流量。
- 必须登录才能上传:否则你的 API 域名会被人当免费图床刷,成本和存储直接失控。
- 业务代码不碰密钥:AK/SK 只存在于环境变量里,代码和仓库里永远看不到。
三、准备工作
3.1 开通产品
开通 函数计算 FC 和 对象存储 OSS(都按量付费,不预购)。
3.2 创建 Bucket
- 名称:全局唯一,例如
my-img - 地域:选离你/你的用户近的,例如
华东1(杭州)→oss-cn-hangzhou - 读写权限:私有(重要)
- 存储类型:标准存储(高频访问的图片就该用这个,别选低频/归档——那些有最短存储时长,删早了反而扣钱)
3.3 创建 RAM 用户并限权到单桶(强烈建议)
很多教程直接让你用主账号 AccessKey——不要这么做。主账号 AK 泄露等于整个阿里云账号沦陷。
RAM 控制台 → 权限策略 → 创建权限策略 → JSON 标签:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"oss:GetObject",
"oss:PutObject",
"oss:DeleteObject",
"oss:ListObjects",
"oss:HeadObject"
],
"Resource": [
"acs:oss:*:*:my-img",
"acs:oss:*:*:my-img/*"
]
}
]
}
策略名 ImageHostSingleBucket。然后:
- RAM → 用户 → 创建用户,勾选 "使用永久 AccessKey 访问"
- 给这个用户只挂上面这一个策略
- 生成 AK/SK,立刻保存到本地密码管理器
几个故意的取舍:
- 不给
oss:ListBuckets—— 泄露后连你有哪些桶都看不到 - 不给
oss:CopyObject—— 图床用不到复制权限,能少给就少给 - 不要挂
AliyunOSSFullAccess—— 那会让你的图床 AK 能读你的所有数据桶
3.4 密钥存放纪律
- ❌ 不要写进代码、不要提交到 Git
- ✅ 只放在 FC 环境变量里
- 仓库根目录的
.gitignore至少要有:
.env
.env.local
*.key
*.pem
*.p12
credentials.json
四、准备依赖层(Layer)
业务代码只依赖 ali-oss。把它做成 Layer,代码包就能保持在几 KB。
方式 A:直接下载 Release 附件(推荐)
项目已发布 Release,layer.zip 与代码包都可直接下载:
https://github.com/ezzty/oss-upload/releases/latest
| 附件 | 大小 | 内容 |
|---|---|---|
layer.zip | 3.25 MB | ali-oss@6.23.0 及其依赖,共 2338 个文件 |
oss-upload-code.zip | 约 6 KB | index.js + package.json,函数部署包 |
好处:不用装 Node.js、不用打 zip,也不会因为你本地 npm install 拿到了更新版本的 SDK 而产生"和作者验证过的不一致"的问题。层内只有 npm 包源码,没有密钥。
方式 B:自己构建
想换依赖版本、或不信任现成包时自己打:
mkdir -p layer-build/nodejs
cd layer-build
npm init -y
npm install ali-oss
# 目录必须是这个结构:
# nodejs/
# node_modules/
# ali-oss/
# ...它的传递依赖
cd nodejs && zip -r ../layer.zip node_modules && cd ..
上传
FC 控制台 → 层管理 → 创建层:
- 名称:
oss-upload-deps - 运行时:Node.js 18
- 上传
layer.zip
层只需创建一次。以后升级 ali-oss 版本,重新打一个版本发布,再把函数指到新版本即可,不用改业务代码。 五、部署函数
5.1 获取业务代码
推荐直接用 Release 附件:https://github.com/ezzty/oss-upload/releases/latest 下载 oss-upload-code.zip(约 6 KB),解压后里面就是标准部署包。
自己打包的话,zip 根目录只能有这两个文件(不要套文件夹,否则 Handler 找不到 index.handler):
code.zip
├── index.js ← 业务源码
└── package.json ← 声明依赖(供参考,实际运行时从层加载)
zip code.zip index.js package.json
5.2 创建函数
- 运行时:Node.js 18
- 处理程序(Handler):
index.handler - 规格:0.05 vCPU / 128 MB 内存 / 512 MB 磁盘(实测够用的最小规格)
- 层配置:绑定刚才创建的
oss-upload-deps
内存这块要留神:代码把整个 body 读进内存(bodyBuf),base64 解码时还会产生一份副本。128 MB 跑常规图片没问题,但如果你把上传上限调到 20 MB 又常传接近上限的大图,内存得跟着往上调。 5.3 环境变量(8 个必填,缺一个都起不来)
代码开头会逐个检查,缺失直接 process.exit(1),函数返回 500 且日志里有 ❌ FATAL: Missing required environment variable: X。
8 个必填(代码里 requiredEnvVars 就是这 8 个):
AUTH_USERadmin—— 登录用户名AUTH_PASS<请设置强密码>—— 登录密码,用随机长串JWT_SECRET<32位以上随机字符串>—— token 签名密钥OSS_ACCESS_KEY_IDLTAIxxxxxxxxxxxxxxxx—— RAM 用户的 AKOSS_ACCESS_KEY_SECRET<你的 AccessKey Secret>—— RAM 用户的 SKOSS_BUCKETmy-img—— 桶名OSS_REGIONoss-cn-hangzhou—— 桶所在地域 IDCDN_DOMAINhttps://img.example.com—— 必须带协议头,否则启动即失败
1 个可选:
ALLOWED_ORIGINSexample.com,myblog.net—— 允许跨域来源,逗号分隔。不设也能启动;它只在启用代码级 CORS 时才参与判断(默认 CORS 由平台直接回显Origin,isValidOrigin根本不会被调用)。若启用代码级 CORS 则必须设它,否则兜底值example.com会把浏览器上传全拦死。

生成随机值:
openssl rand -base64 32 # JWT_SECRET
openssl rand -base64 24 # AUTH_PASS
CDN_DOMAIN必须以http://或https://开头——代码里有专门的校验,忘了写协议头会直接终止启动。
5.4 绑定自定义域名
FC 控制台 → 域名管理 → 添加域名 → 按提示配 CNAME 和证书。这样你才有 https://api.example.com 这样的 API 地址。
六、前端页面
upload.html 是个单文件前端,直接扔到你自己的网站目录即可。
必须改一处,找到 _FC_PARTS:
// 改成你的函数计算自定义域名
const _FC_PARTS = ['https://', 'api.example.com'];
体积上限要和后端对齐(默认都是 20MB):
if (file.size > 20 * 1024 * 1024) { /* 拦截 */ }
前后端各有一道限制,以小的那个为准。只改前端不改后端,大文件会在服务端被 413 掉;只改后端不改前端,用户在浏览器就被拦住了,根本发不出去。
七、想深入?核心细节都在源码 README
以上就是跑通自建图床的完整流程。代码为什么这么写、每层防护的实现细节、完整验收清单,都整理在源码仓库的 README 里,需要时对着看:
https://github.com/ezzty/oss-upload → README.md 的「🧩 核心实现(代码级)」与「🧪 上线验收」
那里有这里没展开的内容:
- Token 结构与四个安全点(HMAC 签名、常数时间比较、防重放的边界)
- 魔数检测对照表(JPEG / PNG / GIF / WebP / BMP 的文件头字节)
- 体积三重校验、登录体 4KB 上限、IP 防爆破为什么必须用
Map(X-Forwarded-For: __proto__原型污染实测) - FC 事件格式兼容与 body 解码(含两个大坑:惰性解码、未编码二进制不能按 UTF-8 解)
- 前端发送方式(iOS Safari 会丢
xhr.send(File)的 body,要改用 ArrayBuffer) - HTTP 触发器与 CORS 的三种方式(含一个澄清:FC 控制台根本没有「勾选允许跨域」这个开关)
- OSS 图片处理(实测 1.89 MB → 81 KB,省 96%)
- 完整上线验收清单(探针命令 + OSS 核实脚本 + 安全 checklist)
- 常见坑速查表(现象 → 原因 → 解决,iOS 丢 body、魔数误判这类疑难杂症也在里面)
博客正文只想讲清楚「为什么自建、怎么跑起来、要注意什么」;真要动手部署,README 就是完整技术文档。
八、成本
按量付费,三块:
| 项目 | 计费方式 | 量级参考 |
|---|---|---|
| FC 计算 | 计算 CU(vCPU+内存×时长)+ 调用次数 | 有免费额度,个人图床通常在额度附近 |
| OSS 存储 | 元/GB/月 | 标准存储约 0.09~0.12 元/GB/月 |
| OSS 请求 | 按次 | 极低,可忽略 |
| 流量 | 元/GB | 见下 |
流量是账单大头,也是最容易优化的:
- 上传到 OSS:免费
- OSS 内网(FC ↔ OSS 同地域):免费
- OSS 外网流出:收费(约 0.5 元/GB 量级,按时段与地域不同)
- 走 CDN:CDN 回源走内网,回源流量免费,只按 CDN 流量计费
结论:图床一定要挂 CDN,既提速又能绕开 OSS 外网流出费。另外注意 FC 与 OSS 必须同地域,跨地域会走公网,既慢又可能产生额外流量。
博主实测,如果一个月上传几十张图,一个月的费用大概为1元钱左右。
九、维护更新
改业务代码(比如图片链接形式):
- 改本地
index.js zip code.zip index.js package.json- 控制台 → 代码部署 → 上传 → 部署
- 跑 README 验收探针确认新版本生效
改前端:
- 改
upload.html - 上传到网站,建议设
Cache-Control: no-cache,否则用户可能拿到旧版
升级依赖:
- 重新
npm install ali-oss并打包layer.zip - 控制台发布新层版本
- 函数切换到新层版本
换密钥(定期做):
- RAM 生成新 AK/SK
- 更新 FC 环境变量 → 部署
- 确认服务正常后,在 RAM 控制台禁用旧 AK