业务背景和问题#
仿小红书吉大校园平台 旧上传接口 安全隐患大,写权限收的不够紧,有水平越权的现象:有闲的没事的同学更改了我们的一些媒体资源 旧读接口 防刷没做好,别碰见家豪给阿里云刷欠费喽😄
阿里云OSS服务 收费标准#
下行流量 >> OSS存储 >> https请求 流量支出是大头!
每个月流量100 gb左右 支出约 xx rmb
量化效果与验证方法#
流量成本(通过查询阿里云控制台)#
从 0.49/GB 降价到 0.13/GB
每 GB 省下了:0.49 - 0.13 = 0.36 节省的百分比:0.36 ÷ 0.49 ≈ 73.47%
图片加载速度#
我在本地写了一个 Shell 脚本,利用 curl -w 命令定义了输出格式。针对同一张测试图片,分别向 OSS 的直连域名和 CDN 域名发起了多次请求,并打点了 time_namelookup(DNS 解析)、time_connect(TCP 建连耗时)、time_starttransfer(TTFB 首字节时间)以及 time_total(总耗时)。
对比数据发现,CDN 的边缘节点调度把建连耗时和首字节时间极大地压缩了。总耗时从直连的 190 多毫秒稳定降到了 30 多毫秒,绝对延迟降低了 160 毫秒左右,算下来提升了 5 倍余。
time_namelookup(DNS 解析):域名翻译成目标服务器 IP 地址所花的时间。time_connect(TCP 建连): 客户端和服务器完成“三次握手”,建立可靠传输通道所花的时间。time_starttransfer(TTFB 首字节):从发出请求,到收到服务器返回的第一点数据所花的时间(反映了服务器的处理速度和网络往返延迟)。time_total(总耗时): 从开始找地址,直到图片完整下载完毕的总时间(也就是用户感受到的白屏加载时间)。
引入CDN的收益#
- 就近接入,所以极大缩短
time_connect,用户体验更丝滑 - 缓存命中,CDN 边缘节点会将热点资源直接缓存在自己的高速内存或 SSD 中。当请求打到边缘节点且缓存命中(Cache Hit)时,节点直接把数据塞给用户,省去了漫长的回源网络等待和源站处理时间,TTFB(首字节时间)因此发生质的飞跃
- 下行流量费用更低,且回源走的公司内网不额外收费
- 防止DDOS: 负载均衡,流量分散;隐藏源站IP
cdn是什么?原理?CDN加速访问怎么做的?#
Content Distribution Network 内容分发网络,顾名思义,就是把静态资源比如图片分发到不同的地方,以达到c端就近访问的目的——比如北京的用户就访问北京的CDN节点而不用访问可能物理距离很远的OSS源站。
其实可以看作是对象存储层的特殊缓存服务,既然是缓存就有一致性的问题,如果用户访问的资源不在cdn边缘节点上,它会自动回源,而且因为回源走的阿里云内网,不收费
CDN工作原理简述#
如何找到最合适的 CDN 节点?#
流程:
- app向用户当前所处网络的本地 DNS 发起解析请求
- 请求最终打到 CDN 的 GSLB(DNS 的迭代查询过程)
- GSLB 根据用户 IP 地址、CDN 节点状态(负载、性能、响应时间、带宽) 等指标,综合判断并返回最优 CDN 节点的 IP 地址。
- App 拿到最优节点 IP,建立 TCP 连接,带着你的
objectKey和auth_key去拉取图片
这里的关键:全局负载均衡 (GSLB):将用户请求调度到最优的 CDN 节点
GSLB 转发机制有三种实现:
- DNS解析 (主流实现,DNS 有缓存功能和负载均衡能力,能天然减轻 GSLB 的压力)
- HTTP重定向
- IP路由
简单举个例子,比如之前网站网址是 www.netitv.com.cn,此时进行要 cdn 改造,那么将之前的网站网址作为 GSLB 服务域名的 CNAME,用户访问 www.netitv.com.cn,经过 CNAME 解析会映射到 GSLB 地址 www.netitv.cdn.com.cn 上,然后 GSLB 基于 DNS 协议可以进行后续的负载均衡操作,选择合适的 IP 返回给用户。
静态资源是如何被缓存到 CDN 节点中的?#
预热和回源
节点上没有用户请求的资源或该资源的缓存已过期,就得回源 运维侧在CDN控制台配置同账号下私有OSS bucket回源,即可实现缓存未命中自动回源
如何防止静态资源被盗用?#
通过检查HTTP请求头中的Referer字段来判断请求来源是否合法 动态URL鉴权 服务器Token鉴权
防盗链机制:推荐采用 Referer 防盗链 + 时间戳防盗链的组合方案,平衡安全性与实现成本
怎么用的 怎么实现的#
开发侧:#
STAR法则
1. 业务背景(抛出痛点) “改造之前,我们的媒体资源链路比较粗暴:
- 数据库里直接存了完整的 OSS 直链,前端展示的也是OSS直链,并设置桶为公开读,这不仅导致数据被严重污染、后续无法灵活更换 CDN 厂商,还非常容易被恶意盗刷流量;
- 上传接口的鉴权做的不好,有水平越权的现象发生 为了解决这个问题,我进行了全站媒体资源链路的重构,核心思想就是遵循 KISS 原则,实现存储与展现的解耦。
2. 高度抽象数据链路(分段阐述,突出亮点) “整个重构后的链路,我按数据的流向分为上、中、下三段:
-
上传侧: 我废弃了传统的宽泛凭证,新增了上传意图接口。App 必须先来后端申请,后端校验合法后,生成一个纯净的
objectKey,并动态签发一个只针对该文件的最小 STS 权限。App 拿到凭证后直传 OSS,不占用服务器带宽。 -
第二段是入库侧: 无论是发帖还是改头像,写入链路都会强制经过我封装的
MediaUrlService。它像一个漏斗,会拦截掉所有的越权伪造和路径穿越,最终 DB 和 Redis 里只落盘最纯净的objectKey。 -
第三段是输出侧: 我在出参的视图层(VO)利用 Jackson 序列化做了切面。数据返回给前端的瞬间,后端会在内存里把
objectKey动态组装成带有 Type A 过期时间签名的完整 CDN URL。” -
至于VO和DTO/POJO的转换和其必要,使用了MapStruct
URL 签名的流水线#
- DB -> POJO(裸数据)
MyBatis 从数据库中查出原生的实体类(Entity/POJO,如
Comment)。这时候,像头像、图片这种字段里装的仅仅是纯文本的objectKey(例如avatar/xxx.jpg),没有任何域名和签名。 - POJO -> VO(挂载规则)
在 Controller 或 Service 返回之前,调用了 MediaResponseMapper(由 MapStruct 自动生成的实现类)把实体映射为 View Object(如 CommentResponseVO)。 注意: 在这一步,objectKey 本身的字符串并没有发生改变。转换的意义在于,VO 对象的字段上挂载了特定的注解规则,比如:
@JsonSerialize(using = CdnUrlSerializer.class)
private String authorAvatar;java这相当于给这个字段贴了一个标签:“嘿,一会儿谁要把我变成 JSON,请用 CdnUrlSerializer 这个工具处理我”。
- VO -> JSON(动态触发签名)
你提到的“网关层”,严格意义上来说其实是 Spring MVC 的 Web 层(HTTP 消息转换器)。
当 Controller 返回了 ApiResponse<VO> 后,Spring Boot 准备把它写入 HTTP 响应体传给前端。这时候它会召唤默认的 JSON 序列化工具 —— Jackson。
- Jackson 开始遍历这个 VO 对象。
- 当它扫描到
authorAvatar字段时,看到了上面贴的@JsonSerialize标签。 - 于是,Jackson 放下了自己默认的字符串输出器,转而实例化了你写的自定义拦截器
CdnUrlSerializer。 CdnUrlSerializer在内部调用了阿里云的 SDK(通过MediaUrlService),把裸的objectKey瞬间加上了https://cdn...域名,并根据当前的服务器时间计算出了一个比如 1 小时后过期的auth_key签名参数。- 最后,Jackson 把这个长长的、带签名的完整 URL 写进了最终的 JSON 字符串里,输出给网络层。
总结你的理解:
- POJO 转 VO 确实是
ResponseMapper(MapStruct)做的,主要是为了脱敏和贴序列化标签。 - 签名转换 确实是自定义的 Jackson(
CdnUrlSerializer)做的。 - 发生的位置不是在独立部署的网关(如 Nginx 或 Spring Cloud Gateway),而是在 Spring Boot 本身的响应序列化阶段(即准备往浏览器吐 JSON 的那一刻)。
3. 用数据收尾(证明价值,并主动抛出诱饵) “通过这套架构,业务代码完全不需要关心图片链接是怎么拼的,做到了高内聚低耦合。同时,边缘的防盗刷加上源站的文件级鉴权,帮我们把公网下行流量成本实打实地降低了 73.4%,图片加载性能也提升了大概 5倍。 在这个过程中,为了保证新老版本的平滑过渡,我也做了一些历史数据清洗和接口兼容的工作,如果您感兴趣,我可以展开讲讲。”
运维侧:#
这里跟着阿里云手册走即可,不确定的问qwen助手他网站上面那个rag
这里有个trick 如果你想快速测试而没有已备案的网站,可以把cdn加速区域选择为非大陆。
还有申请SSL证书,为安全一般强制启用HTTPS,HTTPS按每万次请求收费的。 配置过程略
像ios app 强制拦截非https请求,所以不得不申请
CDN-OSS 鉴权+防刷 架构#
OSS写鉴权#
后端下发STS临时访问凭证#
选择该方案理由#
- 适用性最广:
支持分片上传、断点续传等复杂场景,特别适合仿小红书类项目中用户上传大文件(如图片、视频)的需求。 - 安全性高:
- 临时凭证(有效期通常为几分钟到几小时)相比长期AccessKey更难泄露。
- 可通过自定义策略进一步限制权限(如仅允许上传到指定Bucket目录)。
- 性能优化空间:
服务端可对STS凭证进行缓存(如Redis),减少频繁调用STS服务导致的限流风险。
其他方案对比#
-
PostPolicy + 表单上传
- 适用场景:限制文件类型/大小且无需分片的小文件上传(如网页表单)。
- 缺点:不支持分片,大文件上传易超时。
- 实现:服务端生成
policy和signature,客户端通过HTML表单提交。
-
预签名URL(PutObject)
- 适用场景:简单的单文件上传,且无需分片(如小程序临时文件分享)。
- 缺点:大文件需多次生成URL,交互复杂,不适合以后多图、弱网、分片、断点续传。;客户端可篡改分片内容。
- 实现:简单,服务端通过
signUrl生成带时效的URL,客户端直接PUT上传。
总之,仿小红书的APP项目应优先选择STS临时访问凭证方案,其对大文件和复杂场景的支持、安全性及可扩展性均优于其他方案。
STS动态policy#
此处涉密, 已自动脱敏删去
不做的坏处#
读路径 流量防刷+防盗链#
针对防刷,我没有采用简单的 IP 限流,因为在校园网环境下(共用出口 IP)很容易造成大面积误杀。
我们采用的是阿里云 Type A 动态 URL 鉴权结合边缘频控的组合拳,具体分为三个防御层级:
第一层:无证拦截(阻断直接盗链与遍历猜测) 我们的图片 URL 是动态生成的。当用户侧发起请求前,后端的 Java 业务服务会使用 URI + Timestamp + 随机数 + 预设密钥,计算出一个 MD5 签名拼接在 URL 后面。 如果恶意爬虫直接构造 URL,或者去遍历资源路径,由于它没有我们的鉴权 Key,算不出合法的 MD5,CDN 边缘节点会直接校验失败并返回 403。这一步将 100% 的非法试探流量拦截在了边缘,实现了零回源。
第二层:过期失效(阻断长效重放攻击) 如果黑客通过抓包,拿到了一个合法的、带有签名的 URL 怎么办? Type A 鉴权的 URL 中带有时间戳(Timestamp)。我们在生成时设置了合理的 TTL(比如 30 分钟过期)。黑客抓到的 URL 一旦过了这个时间窗口,签名就自动失效了,无法被长期作为盗链引用。
第三层:边缘高频限流(阻断短时疯狂重放) 那么对于最极端的情况:黑客写了脚本,抓到一个合法 URL 后,在过期前的这 30 分钟内发起高并发的疯狂请求,怎么防? 这时候我们的边缘限流规则就生效了。因为正常的客户端加载一张图片,短时间内只会请求一次;而‘刷子’会在极短时间内对同一个签名 URL 发起成百上千次请求。我们在 CDN 控制台配置了速率限制,如果同一个 IP 针对同一个带 Token 的 URL 在 10 秒内请求超过合理阈值(例如 20 次),CDN WAF 会直接进行 IP 封禁或连接阻断。
通过这三层逻辑,我们既避免了单纯 IP 限流的误杀,又把恶意刷量流量彻底挡在了源站之外。
URL 鉴权方案选择:#
URL鉴权功能主要用于保护用户站点资源不被非法站点下载或盗用
type a、b、c、d底层逻辑依然是 MD5 哈希校验 + 时间戳过期,核心差异完全在于 URL 的拼装格式和参数的灵活度,这里没有孰优孰劣,根据具体业务选择合适方案即可:
按官方文档,Type A 和 Type B 都会在鉴权成功后还原成原始 URL 再做缓存键和回源,所以“缓存命中”不是决定性差异。 区别是:
- Type A:query 参数式,适合现在这种改造小、兼容 OSS 直链的方案。
- Type B:路径前缀式,更像内容站/图床风格,适合你特别想要“无 query、像小红书”的外观。
APP端演进#
App 上传链路统一迁移到新接口 POST /api/files/upload-intents。 App 不再本地拼 OSS 路径,不再使用旧 GET /api/files/sts,不再用 STS 做 OSS GetObject。 上传时 App 只把文件元信息交给后端,使用后端返回的 objectKey 执行 PutObject;业务接口统一提交 objectKey。 展示时 App 只使用后端业务接口返回的 CDN Type A 签名 URL。 图片缓存 key 改成稳定媒体 key 而不是旧版逻辑中完整的url,避免 auth_key 每次变化导致缓存击穿。
后续系统迭代的考量:#
1. “垃圾数据”与孤儿文件问题 (Orphan Files)
-
隐患: 现在的流程是:App 拿到
objectKey-> 上传 OSS -> 调用业务接口(发帖)落库。如果 App 上传完 OSS 后,由于网络抖动、App 崩溃或用户直接杀后台,导致业务接口没有被调用。这时 OSS 里就多了一个真实存在,但数据库里没有记录的“孤儿文件”,日积月累会浪费大量存储成本。 -
大厂解法:
- OSS 生命周期 (Lifecycle): 规定所有客户端直传的文件默认放入一个临时目录(如
tmp/posts/{uid}/...),该目录设置 1 天后自动删除。 - 业务兜底/触发转移: 当 App 调用业务接口(发帖)成功落库后,后端异步发起一个 OSS 的
CopyObject请求,将文件从tmp移动到正式目录,或者通过修改文件的 Header/Tag 让其免于被清理。
- OSS 生命周期 (Lifecycle): 规定所有客户端直传的文件默认放入一个临时目录(如
2. 图像处理与 CDN 边缘计算
- 优化空间: 后端拼接 CDN URL 时,还可以根据 App 端传来的参数动态追加 OSS 的图像处理参数(
?x-oss-process=image/resize,w_800/format,webp),做到原图存储、多端按需裁剪。将裁剪和格式转换交由 CDN 边缘节点或 OSS 回源处理,极大节省带宽并提升加载速度。
3. 内容合规与风控 (Content Moderation)
- 合规要求: 用户直传图片到 OSS,如何防止上传涉黄、涉暴内容?
- 大厂解法: 通常会配置阿里云的内容安全(绿网)。当图片上传到 OSS 后,自动触发 OSS 回调(Callback)或事件总线(EventBridge)推送到内容审核微服务。如果判定违规,直接在后台将该
objectKey标记为不可见或删除。