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 去扛。下载统计怎么数、流量怎么防刷,是第四篇和第五篇的内容。
