Appearance
从 SSL 证书过期到 CDN:一次 OSS 资源地址的来回调整
最开始的问题很直接:自定义 OSS 域名的 SSL 证书过期,导致页面中的图片无法正常显示。
后来运维人员恢复了证书。
证书恢复以后,新请求已经可以正常访问,但部分已经打开的页面仍保持之前图片加载失败的状态,需要刷新甚至强制刷新后才能重新显示。
这里不把原因只归结为“浏览器缓存了 SSL 错误”。
一方面,失败的连接或请求状态可能在短时间内被复用;另一方面,<img> 等资源一旦加载失败,浏览器通常不会因为服务端证书恢复而自动重新发起请求。刷新页面后重新请求资源,图片才恢复显示。
真正值得记录的,不只是证书过期本身,而是后续围绕 OSS 原生域名、自定义域名、CDN 和数据库资源地址发生的一系列调整。
证书为什么会一直过期到影响业务
SSL 证书过期没有在业务受影响以前被发现。
证书到期并不是一个突然发生的事件,它的有效期是提前已知的。
如果只有在图片无法访问以后才发现问题,说明当时证书生命周期还没有真正进入主动监控范围。
至少缺少了下面这些机制中的一部分:
- 证书到期时间检查;
- 到期前提前告警;
- HTTPS 资源域名可用性探测;
- 证书续期责任和处理流程。
代码没有变化,OSS 中的文件也没有变化,但一个外部基础设施依赖到期,同样可以让业务资源整体失效。
第一次调整:改用 OSS 原生域名展示
证书恢复以后,我还是决定把资源展示地址改成阿里云 OSS 原生域名。
当时 OSS 配置大致类似:
java
OssConfig ossConfigCn = new OssConfig();
ossConfigCn.setAccessKeyId("...");
ossConfigCn.setAccessKeySecret("...");
ossConfigCn.setEndpoint("https://oss-cn-hangzhou.aliyuncs.com/");
ossConfigCn.setBucketName("bucketname");示例中的 AK/SK 仅用于说明配置关系,实际凭证不应硬编码在代码中,应通过配置中心、密钥管理服务或临时凭证等方式提供。
同一个对象可以通过 OSS 原生地址访问:
text
https://bucketname.oss-cn-hangzhou.aliyuncs.com/xxx.jpg也可以通过已经绑定到 OSS 的自定义域名访问:
text
https://oss.example.com/xxx.jpg当时选择 OSS 原生域名展示,主要是为了减少对自定义域名证书生命周期的依赖。
配合这次调整,我修改了一批代码,也调整了数据库中的历史资源地址。
当时的思路很直接:
如果直接使用阿里云 OSS 原生域名,就不需要再单独维护自定义资源域名的 SSL 证书。
这个方案降低了证书维护风险,但后来又遇到了新的约束。
第二次调整:上传和展示不是同一个问题
后来访问策略进一步收紧。
上传侧要求只允许使用指定的阿里云 OSS Endpoint,例如:
text
https://oss-cn-hangzhou.aliyuncs.com/而资源展示侧仍然希望使用自定义域名:
text
https://oss.example.com/原因包括:
- 展示资源需要 CDN;
- 浏览器访问需要处理 CORS / CORP 等策略;
- 自定义域名更适合作为统一、稳定的资源展示入口;
- OSS Endpoint 更适合作为应用访问对象存储服务的基础设施地址。
其中,CORS 用于控制跨源请求权限,CORP(Cross-Origin Resource Policy)用于限制资源是否允许被其他来源加载,两者作用层次不同。
这时我才意识到,之前一直在讨论“到底应该用哪个域名”,其实把两个不同问题混在了一起。
上传地址
上传时使用 OSS Endpoint。
例如:
text
https://oss-cn-hangzhou.aliyuncs.com/它负责的是应用和对象存储之间的交互。
链路更接近:
text
应用
↓
OSS Endpoint
↓
Bucket
↓
Object这里关注的是:
- OSS SDK;
- Endpoint;
- Bucket;
- 权限;
- 上传策略;
- 网络访问限制。
展示地址
浏览器展示资源时使用自定义域名:
text
https://oss.example.com/xxx.jpg链路更接近:
text
浏览器
↓
自定义域名
↓
CDN
↓
OSS这里关注的是:
- HTTPS;
- CDN;
- 缓存;
- CORS;
- CORP;
- 浏览器资源加载策略。
上传和展示最终指向的是同一个对象,但承担的职责不同。
因此,上传域名和展示域名没有必要保持一致。
关于读权限的前提
本文讨论的是可以通过固定 URL 直接读取的资源场景。
如果 Bucket 为私有读,展示时需要生成带签名和有效期的 URL。此时数据库仍然可以只保存对象路径,但最终展示地址需要经过签名层生成。
数据库里真正需要保存什么
如果数据库中保存完整 URL:
text
https://oss.example.com/xxx.jpg后来改成:
text
https://bucketname.oss-cn-hangzhou.aliyuncs.com/xxx.jpg就需要修改历史数据。
以后如果再切回自定义域名,又可能需要再改一次。
但整个过程中真正没有变化的是:
text
/xxx.jpg对象还是那个对象。
变化的只是当前通过哪个入口访问它。
因此后来我把这几个概念拆开:
text
OSS Endpoint
资源对象路径
展示域名它们分别属于不同层次。
OSS Endpoint
例如:
yaml
oss:
endpoint: https://oss-cn-hangzhou.aliyuncs.com/用于上传以及访问 OSS API。
数据库资源路径
数据库只保存:
text
/xxx.jpg或者:
text
xxx.jpg它代表资源本身的位置,不包含当前展示策略。
展示域名
展示域名放到配置中:
yaml
oss:
display-base-url: https://oss.example.com最终返回前端时再组合:
text
display-base-url + object-path得到:
text
https://oss.example.com/xxx.jpg拆开以后,域名切换不再需要改历史数据
调整后的关系变成:
text
数据库
↓
/xxx.jpg
上传
↓
OSS Endpoint + /xxx.jpg
展示
↓
Display Base URL + /xxx.jpg以后如果展示域名从:
text
https://oss.example.com改成:
text
https://cdn.example.com只需要修改配置。
数据库中的:
text
/xxx.jpg不需要变化。
如果临时需要切回 OSS 原生域名展示,也只需要调整展示域名配置。
这样,资源访问入口变化不会再扩散到历史业务数据。
为什么不能简单理解成“数据库不要存完整 URL”
完整 URL 本身并没有问题。
如果保存的是一个真正的外部链接,例如:
text
https://example.org/article/123这个 URL 本身就是业务数据,那么完整保存是合理的。
这次需要拆开的,是自己管理的 OSS 资源。
对于这类资源:
- 对象路径代表资源本身;
- OSS Endpoint 代表上传和存储访问方式;
- 自定义域名代表当前展示方式。
如果把三者合成一个完整 URL 持久化,基础设施变化就会扩散到业务数据。
最后的结构
调整后,资源访问关系变成:
text
上传:
应用
↓
OSS Endpoint
↓
Object
持久化:
数据库
↓
Object Path
展示:
浏览器
↓
自定义域名
↓
CDN
↓
OSS Object这样既可以在上传侧限制只能访问指定 OSS Endpoint,也可以在展示侧继续使用自定义域名、CDN 和浏览器安全策略。
沉淀下来的约束
- 数据库保存资源标识,不保存当前展示入口;
- 上传入口和展示入口分别配置,不要求使用同一个域名;
- 自定义域名一旦进入业务链路,证书生命周期必须进入主动监控。
后续需要补上的监控
如果继续使用自定义域名作为展示入口,后续至少需要覆盖:
- 自定义域名证书到期时间;
- 到期前提前告警;
- HTTPS 可用性探测;
- CDN 资源访问检查;
- 证书续期责任和处理流程。
自定义域名带来了 CDN、统一入口等能力,也意味着需要承担对应的运维责任。
结论
实际经历的过程是:
text
自定义域名 SSL 证书过期
↓
图片无法显示
↓
运维恢复证书
↓
部分已打开页面需要重新刷新
↓
改用 OSS 原生域名展示资源
↓
修改代码和历史资源 URL
↓
访问策略进一步收紧
↓
重新区分上传地址与展示地址
↓
上传使用 OSS Endpoint
展示使用自定义域名 + CDN
数据库只保存对象路径最终稳定下来的,是三个独立概念:
资源本身、资源上传入口、资源展示入口。