把图片搬上云:一次 OSS 接入与站点收尾记录

博客主体稳定之后,剩下的都是「打磨」的活儿:图片该放哪、加载为什么慢、主题里还留着谁的示例内容、后台够不够稳、服务器还能不能更收紧一点。这篇记录这一轮的完整过程。

起因:图片不该继续躺在 NAS 里

站点跑顺之后,几个问题一直悬着:

  • 文章封面用的是第三方随机图接口,时快时慢,偶尔还失效,稳定性全看别人脸色;
  • 主题自带的一批示例图片、二维码、头像都放在 NAS 本地,重装主题时还容易被覆盖;
  • 首页首屏加载偏慢,排查下来慢的就是那几张境外/随机图;
  • 新上传的附件默认还是落本地,NAS 存储一直在涨。

目标很明确:图片统一进对象存储,走 CDN/原生域名访问,NAS 不再承担图片存储与分发。

接入对象存储:先踩一个 403

Halo 的存储策略是可插拔设计,安装「对象存储(S3 协议)」插件后,新建一个存储策略即可,阿里云 OSS 兼容 S3。

配置时最容易翻车的是 Endpoint 访问风格:

❌ Path Style        → oss-***.aliyuncs.com/***/xxx.jpg
                       返回 403 SecondLevelDomainForbidden
                       "Please use virtual hosted style to access."

✅ Virtual Hosted    → ***.oss-***.aliyuncs.com/xxx.jpg
                       正常工作

一开始按惯性选了 Path Style,验证直接 403。用一段独立的签名脚本绕过插件、直接对 OSS 做 ListBucket / PutObject / GetObject 测试,才定位到:公共 Endpoint 只接受 Virtual Hosted Style。改过来之后,三项操作全部 200。

经验:排查这类问题时,先用一个最小脚本直连对象存储,把「是权限问题、配置问题、还是插件问题」分清楚,比在界面里反复试快得多。

一个隐藏的坑:域名没解析

策略验证通过后,上传返回的图片链接却打不开。查下去发现,配置里填了一个自定义加速域名,但这个域名根本没有 DNS 解析记录,也没有真正配置 CDN —— 于是所有图片链接都是 404。

修法很直接:清空「绑定域名」,让图片回落到 OSS 原生域名。验证:

✅ 上传成功  external-link: https://***.oss-***.aliyuncs.com/.../xxx.png
✅ 公网读取  HTTP 200

还有个小细节:清空绑定域名时,协议字段不能一起清空,否则插件会 500——protocol 保留 https,只清 domain。

设为默认策略这一步也有讲究:运行时的默认策略写在生效的 system 配置里,而不是出厂的 system-default 模板里;并且要先停服务再改配置,否则启动流程会把改动覆盖回去。

封面重做:三张图,一次换完

不再依赖随机图,直接按文章主题生成了三张轻量 SVG 封面:

文章 封面设计
把博客搬回家 深蓝渐变 + 云/NAS 方块 + 隧道连线
ECS 安全加固 深红渐变 + 盾牌 + SECURITY
从零搭建博客 青绿渐变 + 代码窗口 + BUILD FROM ZERO

每张只有 1~2 KB,加载几乎无感。上传进 OSS 后,Halo 三篇和自研博客三篇的封面统一换成同一批地址,两个站风格对齐,旧的随机图引用清零。

之前:https://第三方随机图/xxx.webp   (慢、会失效)
现在:https://***.oss-***.aliyuncs.com/covers/xxx.svg

主题细节收尾

顺手把 Hao 主题里残留的模板作者示例内容清掉:

  • 公众号二维码换成自己的真实二维码;
  • 署名里的「程序员小航」在文章版权声明、关于页面两处,统一改成「程序员刘工」;
  • 补齐 ICP 备案信息,显示在页脚;
  • 站点名、建站时间、首屏打字机文案、明暗主题配色,都按本站定位调整过。

还额外做了两张公众号关注卡片(绿底版 + 白底版),二维码用自己的,搜索关键词换成「刘工开发」。

瞬间:补了十条日常

「瞬间」插件之前是空的,补了十条技术日常,内容覆盖架构迁移、frp、隔离、备份、踩坑记录等,按时间线分布,让侧栏和瞬间页不再是空白。

稳定性打磨:自研博客的两轮排错

对自研博客做了一次比较系统的体检,分两轮修掉了七个问题。

第一轮 · 接口健壮性

问题 修复
page=abc 触发 NaN 拼进 SQL 报错 加 safeInt() 参数校验,非法值回退默认
pageSize 无上限可全量拉取 限制上限 100
标题里的 <script> 原样存储返回(XSS) 输出时用 sanitizeTitle() 剥离标签
超长标题触发 SQL Data too long 并暴露错误 前置校验,返回 TITLE_TOO_LONG
未登录上传返回 200 改为正确的 401/501

第二轮 · 数据与安全

  • 站点设置里 favicon、nav_items 被存成了 [object Object](对象没序列化):清理脏数据,并在写入时自动 JSON.stringify、读取时过滤历史脏值;
  • 登录接口没有速率限制,可暴力破解:加了 IP 级限流,5 次失败锁定 15 分钟,返回剩余次数与等待秒数。

排查过程中也澄清了几个「看起来像 bug、其实正常」的点:伪造 token 返回 HTTP 200 但 body 是 ok:false(API 设计如此,鉴权其实生效);路径遍历返回首页 HTML(Nginx fallback,无泄露);文章页无侧栏是有意的单栏阅读布局。

回归测试 24/24 通过,首屏与主要接口响应都在 100ms 内。

安全收尾:SSH 收回密钥登录

之前为了调试临时给服务器开了密码登录,这一轮收尾把它关掉:

  • PasswordAuthentication 改回 no,保留 PubkeyAuthentication yes;
  • 改前备份配置、sshd -t 语法校验通过后再 reload(不中断现有连接);
  • 用全新连接验证密钥登录正常,再确认密码登录返回 Permission denied (publickey) —— 确认没把自己锁在外面才收工。

小结

这一轮没有动架构主干,做的都是「让它更稳、更干净、更安全」的事:

  1. 图片统一进对象存储,NAS 不再存图,链接全部可访问;
  2. 封面与主题示例内容换成自己的,两个站风格统一;
  3. 自研博客补齐健壮性与安全短板,参数、XSS、限流、数据序列化都收口;
  4. 服务器登录方式收紧,只留密钥。

以及一条贯穿始终的教训:每次改配置前先备份、改完先验证、涉及登录方式时务必留好应急通道。今天所有能一次过的地方,基本都是因为提前留了退路。