2.4 下载链路:私有读与签名 URL

场景:公开桶被盗刷的那一夜

网盘刚上线时,为了方便我把桶设成了公有读。分享链接直接给 COS 原始域名,谁都能下,看起来省事。直到有一天早上看账单,一晚上跑掉了几百 GB 的下载流量——有人写了个爬虫照着桶的列举接口一顿扫,所有文件被批量拖走。损失几百块是小事,文件被人拿去乱传才是大事。

从那天起桶全部切成私有读,任何下载都必须带签名。代价是每个链接都要后端生成,但比起被盗刷,这点麻烦不算什么。公开桶只有在一种场景下合理:内容本身就是公开素材且经过审核,我们的业务不符合。

方案:三种下载鉴权方式

私有读之后,怎么把文件给到用户,有三种实现:

方式 谁签名 有效期控制 适用场景
后端代理下载 后端 接口级 需要强统计的场景
COS 签名 URL 后端发 URL,COS 校验 分钟级可配 我们的主力方案
CDN 鉴权 URL 边缘节点 分钟级 公开下载加速

代理下载最保险但最贵,所有流量又一次经过后端,直传省下的优势全没了。签名 URL 让浏览器或客户端直接从 COS 取,后端只发链接,和上传链路对称。CDN 鉴权在 4.2 CDN 加速一节展开,本节聚焦 COS 签名 URL。

签名 URL 的参数与有效期也要按场景分档:

场景 有效期 理由
页面内预览 5 分钟 看完即弃
普通下载 15 分钟 覆盖大多数下载耗时
邮件附链 2 小时 邮件打开有延迟

配置:Go 生成签名下载链接

COS SDK 生成签名 URL 只需要一行,但有几个参数值得注意:

// 生成带签名的下载地址,有效期由参数控制
url, err := client.Object.GetPresignedURL(ctx, http.MethodGet, key,
    secretID, secretKey, 15*time.Minute, nil)

// 需要强制下载而非浏览器预览时,加上响应头参数
opt := &cos.PresignedURLGetOptions{
    ResponseContentType:        "application/octet-stream",
    ResponseContentDisposition: fmt.Sprintf("attachment; filename=%s", name),
}

Content-Disposition 这一行很关键:不加它,浏览器会尝试内联预览图片和 PDF;加了它,浏览器老老实实弹出下载框。文件名记得做 URL 编码,中文文件名不编码会在某些浏览器上乱码。

前端拿到 URL 后直接跳转或用 a 标签下载即可,不需要任何 SDK。这和上传的客户端直发形成了完美的对称:请求权由后端发,字节由云端走。

踩坑:下载链路的四个坑

第一个坑,签名 URL 当永久链接用。有同事把签名链接贴进公告邮件,两天后用户来投诉说链接失效。签名 URL 的时效是特性不是 bug,要长期有效的分享请走分享系统走短链与提取码,别用裸链接,具体在 4.1 分享链接一节。

第二个坑,签名 URL 被缓存。把签名链接塞进 CDN 或浏览器长缓存,过期了缓存还在,新用户拿到旧链接 403。签名 URL 一律不缓存,Cache-Control 设为 no-store。

第三个坑,Range 请求与签名。大文件用播放器或断点下载时会发 Range 请求拉某一段,早期实现里签名只覆盖了完整 GET,Range 请求被拒。COS 签名是覆盖整个对象的操作权限的,但客户端有些失败重试逻辑会漏掉 Range 头,排查时注意。

第四个坑,流量账单被由来路不明的下载吃掉。私有读挡住了扫描,但持有合法签名 URL 的人可以无限次下载。高价值文件我们加了访问频率限制与日志分析,发现异常下载模式后及时封禁分享,这套机制在 5.3 监控告警里。

下载防盗刷还有几招可以叠加。第一是域名分离:下载用独立域名,主站用它做风控埋点,识别出异常 UA 或频率可以直接在边缘拦截。第二是频率限制:同一分享链接每分钟下载次数超过阈值就要求输入验证码,对真人没影响,对脚本很致命。第三是日志审计:COS 的访问日志可以投递到日志服务,写几条规则告警,半夜被刷也能十分钟内被叫醒。

大文件下载的体验靠 Range 请求撑着。浏览器原生下载不支持断点,但只要服务端返回 Accept-Ranges 头,很多客户端工具就能接着断点拉。COS 默认支持 Range,但要注意签名 URL 的有效期覆盖整个续传过程,否则续到一半链接过期。我们给大文件下载统一发 30 分钟的证,宁可签长一点也不能让用户传到 90% 失败。

再聊聊内容预览与下载的分界。图片和 PDF 直接在签名 URL 上打开浏览器就能预览,不需要额外开发。Office 文档不行,要么接 CI 的文档转码要么给出下载链接。我们折中处理:图片与 PDF 内联预览,其余文件一律下载优先,需要预览的走第三方转码服务。说白了先把 80% 的需求满足,剩下的按真实呼声演进,而不是一开始就追求全格式预览。

最后提示一个计费细节:同地域内服务器到 COS 的内网流量免费,跨地域才收钱。我们曾经把后端部署在另一个可用区,所有 ListParts 与查询都走跨地域,每月多出一笔费用。迁移到同地域后这部分归零,积少成多。架构图上画一条线容易,费用上可能就是四位数 关于下载统计与签名 URL 的关系也想澄清一句。签名 URL 本身不带计数能力,COS 只认签名不认人,统计需要另外搭。我们早期的做法是统计签名 URL 的签发次数,但那只是生成次数不等于实际下载完成次数。真正准的数字来自访问日志分析,这部分在 4.4 下载统计一节有完整方案。如果只是内部看个大概,签发次数也够用,别强求一步到位。

最后讲一个心灵的妥协。网盘上线初期,产品同学要求所有下载必须先登录,理由是露出品牌。我据理力争保留了游客下载模式,理由是分享场景下对方往往没账号也不想注册。后来的数据显示,七成的下载来自游客,如果当初强登录,这批流量至少流失一半。技术和产品互相退一步,用户才能往前走一步。 再补一个踩过的小坑:文件名编码。Content-Disposition 里的中文文件名必须做 URL 编码,而且不同浏览器对编码格式的接受还不一样。某次更新后 Safari 用户下载的文件名全是乱码,排查发现是 RFC 5987 的 filename* 参数没加。跨浏览器的文件名处理是玄学,直接放弃自己写,用业内成熟的库是正解,别在这上面浪费人生。

另外提醒大家关注下载失败的可观测性。用户说下不了,最怕的就是对方说不清、你看不到。我们在签名 URL 下载里埋了客户端回调,下载成功失败都上报一条,后端按文件聚合成功率,低于 95% 自动告警。这套机制抓出过 CDN 节点故障、签名过期、运营商劫持三类问题,没有它只能靠用户自发投诉。 下载链路还有个选择:要不要在后端做一层中转统计后再跳转。早期我们走了中转,每次下载先到后端记个数再 302 到签名 URL,结果高峰期后端带宽被打爆,纯属自找。正确做法是日志聚合统计,把请求交给云端,统计数据从 COS 访问日志或 CDN 日志里分析。后端做协调,不做导管,这句话在心里重复了一百遍。

再啰嗦一遍私有读的好处:它是整套分享体系的地基。分享链接可以随时作废、提取码可以改、有效期可以调,这一切的前提是文件本身不公开。桶权限一旦设成公有读,上面所有控制都成了纸糊的栏杆,谁都能跨。治理这件事永远从最底层开始,先把读权限收回来,其余功能才有意义。 对了,还有一类资源要走另一条路:配置文件、小图标这类频繁访问且公开的素材,仍然可以放公开桶配 CDN。私有读针对的是用户数据,不是所有资源。一刀切的权限策略既费钱又难用,真正通用的是分层:公开资源走 CDN,机密资源走签名,研判哪些该公开是产品设计的活,不是运维拍脑袋。 末了补一句跟大家交底:私有读配合签名 URL 这套链路,我们用了一年多没再出过安全事故,唯一一次告警是用户自己把带签名的邮件转发到了群里。技术能防的系统性问题都防住了,剩下的是人的问题,而人的问题只能靠制度与提醒去缓解。

签名 URL 的有效期我们做过 A/B 测试。半小时过短,用户点开旧链接经常过期;两小时过长,泄漏窗口拉大。最终落在四十分钟,配合页面内自动续签,过期再无投诉。

下载完成率的统计也很有意思。上线签名下载后,我们发现大文件完成率只有六成,其余停在半路。深挖发现是部分客户端不支持带签名的分片下载,做了客户端兼容提示后,完成率回到九成以上。

签名 URL 还要防一个前端常见错误:把签名带在地址里分享出去,等于把钥匙挂在门上。我们做了敏感操作必须经过身份确认的产品兜底,技术之外再补一道产品防线。

小结

私有读加签名 URL 是下载链路的标准答案:安全归签名,速度归直连。唯一需要操心的是有效期的分档和链接的分发纪律,剩下的让 COS 去扛。下载统计怎么数、流量怎么防刷,是第四篇和第五篇的内容。

延伸阅读