解决阿里云 OSS 无 CDN 时的流量风险
立泉对个人博客而言阿里云的 OSS 对象存储服务非常适合存放图片、视频、JS 和 CSS 之类的静态资源,尤其非备案的海外站点,把资源托管在国内云上是在无法使用 CDN 的条件下加速访问的几乎唯一解,从我 2 年的使用体验来看对速度和稳定性都十分满意。
OSS 并不“廉价”,但普通博客存储的数据不会很多,流量不会很大,以标准存储 + 按量计费计算,每 GB 存储 0.12 元,每 GB 流量 0.25 ~ 0.5 元,我的博客每月费用不过 1 元左右,约等于免费。
非正常
正常费用可以忽略,但存在一些“非正常”情况费用可能高得吓人。最近看到一些恶意刷 OSS 流量的帖子,有人一晚上几个小时被盗刷(下载)几 TB 流量产生几千元账单:
很多案例的共同点是阿里云在用户余额不足的情况下并没有及时停止服务,而是随流量的不断增加继续产生账单,一叠加就成百上千元。但是它的计费说明里明明写着欠费后停止服务,为什么还会出现这种情况?
原因是计费周期和账单处理时延,阿里云在《降低因恶意访问流量导致大额资金损失的风险》里有提到,虽然 OSS 会在用户欠费时停止服务但这个“欠费”并非实时。按量付费的账单周期是 1 小时,加上处理账单的时延,受到攻击时的峰值流量可能 1 小时后才会生成账单扣费,“余额不足”的判断也要在 1 小时后出现,这个时候即使停止服务在此时间差里消耗的流量也会产生费用,导致余额为负值。比如在 7:00 ~ 8:00 受到攻击,因为账单周期和处理时延可能直到 9:00 才生成账单,此时已经被攻击 2 个小时,以普通家庭的千兆带宽被盗刷 TB 级流量是很轻松的。
解决
有些案例阿里云会减免部分受攻击的费用,但毕竟它确实有向运营商付出外网流量成本,所以剩余费用依然需要用户支付。对使用 OSS 的小博主,这种巨额流量像一把悬在头上的达摩克利斯之剑,随时落下。
首先 Referer 防盗链对恶意流量攻击是无效的,伪造 Referer 非常简单,它只能起到防止被其它站点盗链的作用。此外最直观的解决思路是流量封顶、带宽限制和 IP 请求数限制,但奇怪的是包括阿里云在内的大型云服务商基本都不给对象存储提供这类细粒度控制功能。而其 CDN 内容分发网络却提供,流量费用更便宜大约只有 OSS 一半,所以外套一层 CDN 配合流量、带宽控制手段即是应对这种攻击的最佳方案。
2025 年 08 月 31 日更新:关于对象存储不支持细粒度控制的原因,参考 AWS 的 S3 甚至不能对自定义域名启用 HTTPS,必须搭配外层的 CloudFront 或 API Gateway + Lambda。可能在云厂商的产品定义中对象存储的主要任务是数据的存储和检索,只具备部分 Web 服务能力,面向互联网的内容分发则应由全功能 CDN 负责。
另一个问题是大陆法规不允许非备案站点使用国内 CDN,如果用国外 Cloudflare 这类无需备案、无限流量的免费 CDN 在国内访问则是南辕北辙。原本低延时的 OSS 请求经 CDN 到国外绕一圈虽然面对流量攻击无需回源不产生流量费用,但速度下降与使用 OSS 的加速初衷已经背道而驰。
上面确认阿里云没有给 OSS 提供细粒度用量控制,但文档提到可以通过云监控设置对 OSS 实时运行状态的报警规则。比如连续多少时间请求数、流量达到多少时触发报警,在通知用户的同时可触发一个 HTTP 请求,配合 OSS 的控制 API 把 Bucket 权限由公开访问变为 Private 私有,这样停止对外服务流量攻击自然失效。
是的,这就是非备案个人博客低成本解决 OSS 流量风险的唯一方法,实时监控、触发报警、停止服务,我可以接受临时停服,不接受飞来账单😶。
函数计算
按照以上思路,需要搭建 HTTP 服务执行 OSS 权限变更操作,基于 Serverless 的 Function Compute 函数计算是一个选择。Serverless 即无需购置服务器,只要提供一个匹配 HTTP 请求的程序,它会在云服务商收到请求时被执行,其余时间不消耗计算资源也不产生费用,是按量付费思路的最优解。
阿里云函数计算费用非常低,调用价格 0.009元/万次,算力价格 0.00009元/vCPUx秒,内存价格 0.000009元/GBx秒,对很少触发的监控需求是等于 0 的。
一个调用 OSS 权限变更 API 的示例:
// Ktor 的 HTTP server
fun Application.configureRouting() {
routing {
// 处理报警触发的 POST 请求
post("/oss-shutdown") {
LogUtil.debug("OSS shutdown start...")
val timeMs = measureTimeMillis {
// 执行时间可能长达数秒
shutdownOSS()
}
LogUtil.debug("OSS shutdown done, time = $timeMs ms")
call.respondText("response to request: done")
}
}
}
private fun shutdownOSS() {
// OSS 的外网接入点,这个是杭州地区的
val endpoint = "https://oss-cn-hangzhou.aliyuncs.com"
// RAM 用户的访问ID和密钥,用于鉴权
val accessKeyId = "****************"
val accessKeySecret = "****************"
val credentialsProvider: CredentialsProvider = DefaultCredentialProvider(accessKeyId, accessKeySecret)
// 阿里云提供的 OSS SDK
val ossClient = OSSClientBuilder().build(endpoint, credentialsProvider)
try {
// 设置存储 Bucket 的读写权限为 Private 私有,终止外部访问
ossClient.setBucketAcl("bucket name", CannedAccessControlList.Private)
} catch (e: Exception) {
LogUtil.error("OSS shutdown error, $e.cause")
} finally {
ossClient?.shutdown()
}
}
不同云服务商的函数计算接口不同,很多需要引入 SDK 按特定方式接收 Event 事件。阿里云支持的自定义运行时则更通用化,只是转发请求,能使用流行的 Spring、Ktor 等框架。HTTP 触发器收到请求会在指定配置的实例中启动用户上传的程序,把请求转发到其 80 端口,收到响应即判断任务完成,然后终止程序。
注意函数计算启动的 Ktor 生命周期和在普通服务器上并不一样,返回 HTTP 响应后会被立即终止,抛入后台线程的任务不会存活,所以必须执行完所有操作才能返回响应。而且函数默认是同步调用,超过 5 秒不回复报警请求,函数计算收到超时反馈也会导致程序在未完成的情况下被终止。
函数计算中与同步调用相对的是异步调用,阿里云会对 Header 里标识异步调用的请求先返回 202,再触发对应程序的后台执行,更适合处理耗时任务。操作 OSS 的 JVM 程序从冷启动到执行完成耗时数秒(本例是 2 秒),为避免报警等待超时应该定义一个轻量的同步 Trigger,让其在触发异步任务后立即返回,这样既能启动耗时操作也不会让报警请求等待过长时间。
创建 Trigger 是因为报警配置只能指定 URL 不能定义 Header,导致不能触发异步调用,所以需要中间人。
做完测试一下,如果成功那把悬在头上的达摩克利斯之剑即可扔掉了。