记我将博客的图片存储迁移到 Backblaze
之前我一直把博客的图片放在个人的 OneDrive 上,然后用嵌入功能得到外链放在博客里。但是考虑到国内访问 OneDrive 的延迟还是偏高,以及不折腾不舒服的心理作祟,于是乎在三月份的时候,我把博客的图片从 OneDrive 迁到了 Backblaze 对象存储。
前期准备
开始之前,我们需要准备好这些东西:
- 一个 Backblaze 免费账号
- 一个 CloudFlare 免费账号
- 一个域名
- 还有你的好心情 :-)
可能你会担心用对象存储是不是会产生高额的账单,或者会因为超出配额导致图片全部无法加载。说实话我之前不敢用对象存储就是因为有这方面的顾虑,但是在 Backblaze 这里我们完全不用担心。首先,我们用的是免费的账户,而且 Backblaze 甚至不要求你添加信用卡。此外,Backblaze 和 CloudFlare 都是带宽联盟的成员,意味着 Backblaze 与 CloudFlare 之间的流量全部是免费的。
在 Backblaze 创建存储桶并上传图片
登录进 Backblaze 的 B2 Cloud Storage 之后,点 Create a Bucket 创建一个存储桶就行了。为了防止被人恶意刷流量,我建议创建一个私有的存储桶。加密和对象锁都不需要。
创建成功后,打开这个存储桶的 Bucket Settings,在 Bucket Info 中添加 {"cache-control":"max-age=43200"} 来配置桶的缓存时间。虽然流量不要钱,但是能环保还是环保一点比较好不是?
因为我们创建的是私有存储桶,所以需要创建一个 Application Key 来允许第三方服务访问这个桶。虽然 Backblaze 默认提供了一个 Master Application Key,但是这就像天天用 root 登录 Linux 主机一样,只有中午才能用,因为早晚会出事。在 Application Keys 页面,点 Add a New Application Key,Allow access to Bucket(s) 里面建议选我们这个桶而不是 All,权限当然是 Read and Write。创建成功之后,注意保存好 keyID 和 applicationKey,因为 applicationKey 只会显示一次。
然后需要下载一个支持浏览对象存储的工具,比如我用的 S3 Browser。然后在 S3 Browser 中新建一个连接,REST Endpoint 填写存储桶的 Endpoint,Access Key ID 就是刚才记下来的 keyID,Secret Access Key 就是 applicationKey。
如果 S3 Browser 可以成功连接到刚才创建的存储桶,那就说明配置正确了。这时候就可以想好目录结构,以及上传图片了。比如我选择把图片按照对应的博文来分类,每个有图的博文都有一个对应的图片目录。
在 CloudFlare 中配置域名
在到 CloudFlare 配置域名之前,我们先要知道指向一个文件的完整 URL。进入 Browse Files 页面,然后进入这个存储桶,接着随便挑一个文件,点它最右边的详情图标,这里的 Friendly URL 就是我们要找的东西。记下 URL 里面的域名,我们接下来要用到。
接下来就可以到 CloudFlare 里面创建一条 CNAME 记录,并把刚才记下来的域名填到目标里面,并且启用 CloudFlare 的代理,这样我们才能享受到带宽联盟的优惠。此外,我们还会针对这个域名配置一些规则,这也需要打开 CloudFlare 的代理开关。
要注意这里只能是二级域名,如 blog-static.boris1993.com,而不能是多级的(blog.static.boris1993.com),否则 CloudFlare 会无法申请证书,也就无法正常启用 HTTPS。
这时候我们就可以用 https://sub-domain.your-domain.com/file/folder-name/image-name.png 访问这个图片了,但是目前我们只能得到一个 401 页面,因为我们必须要带上一个 Access Token 才能访问私有存储桶的文件。
为请求配置 CloudFlare 规则
前往 CloudFlare 的规则页面,选择转换规则(Transform Rule),然后在重写URL 这个 tab 中新增一个规则。
首先,我希望我可以直接用 https://blog-static.boris1993.com/folder-name/file-name.png 就能访问到图片(因为这样看起来更好看),所以我配置了一个路径重写,如果路径中不包含 /file/bucket-name,那么就在路径中补上这一段。
选择路径的重写到,表达式类型选择动态,表达式填写 concat("/file/blog-pics", http.request.uri.path)。这样 CloudFlare 就会自动补全完整的路径。
然后就是访问私有存储桶的 Access Token。Backblaze 支持把 Access Token 放在 Authorization 这个 query parameter 中,所以我们可以选择查询的重写到,表达式类型选择静态(Static),值目前可以随便写,因为你就算现在拿到一个 token,在 24 小时后也是会过期的,所以后面我会讲怎么用 CloudFlare Workers 来更新这个字段。
接下来,根据 Backblaze 官方的建议,我们需要对响应头做一些修改。
切换到修改响应头,新增这样一条规则:
首先要正确配置 Access-Control-Allow-Origin,来避免跨域问题,我偷懒了直接配了个 *,不知道这么配会不会有盗链的问题,暂时先这样吧。
其次 Backblaze 建议修改 cache-control 这个 header,来延长缓存的有效时间。
最后,需要从响应头中删掉一些 Backblaze 的 header 来增强安全性。
为了方便,我把要删掉的 header 放在这里:
- x-bz-content-sha1
- x-bz-file-id
- x-bz-file-name
- x-bz-info-s3b-last-modified
- x-bz-info-sha256
- x-bz-info-src_last_modified_millis
- x-bz-upload-timestamp
同时我为了能让浏览器缓存这个图片,我还让它添加了 ETag 这个 header,但是我在浏览器里一直看不到这个 header,如果有大佬知道为什么,还请不吝赐教。
自动更新访问存储桶的 Token
因为后面要修改规则的内容,所以先得拿到规则集和规则的 ID。规则 ID 好办,打开重写URL 规则的编辑页面,我们就能在 URL 的最后一段得到这个规则的 ID。但是规则集 ID 只能调 CloudFlare API 取得。
1 2 |
GET https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets Authorization: Bearer YOUR_CLOUDFLARE_API_TOKEN |
把 YOUR_ZONE_ID 替换为你的域名的区域 ID,以及把 YOUR_CLOUDFLARE_API_TOKEN 换成你的 API 令牌。我当时因为不知道这个 API 需要哪些权限,始终创建不出带有正确权限的 API 令牌,所以干脆用了 Global API Key。
这个请求会返回一系列规则集,有 CloudFlare 内部的,也有我们自己的。理论上,名字是 default 并且 phase 是 http_request_transform 的那个就是我们要的。但是为了确认,可以再执行这个请求:
1 2 |
GET https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/RULE_SET_ID Authorization: Bearer YOUR_CLOUDFLARE_API_TOKEN |
跟上条请求一样,替换掉 YOUR_ZONE_ID 和 YOUR_CLOUDFLARE_API_TOKEN,以及将 RULE_SET_ID 替换为上面找到的规则集的 id。执行后会返回这个规则集下的规则。如果返回内容中有我们之前创建的那条重写URL 的规则,那么这就是我们要找的规则集。
然后为了安全起见,我们要为这个 CloudFlare Worker 创建一个 API 令牌。进入我的个人资料 –> API令牌,然后点击创建令牌,在接下来的页面中中选择创建自定义令牌,然后如图创建一个令牌。
添加成功后,妥善保存这个令牌。
接下来前往 CloudFlare Workers,创建一个新的 Worker。然后到设置 –> 变量,添加如下环境变量:
| 变量名 | 值 |
|---|---|
| B2KeyID | Backblaze 的 keyID |
| B2AppKey | Backblaze 的 applicationKey |
| B2BucketName | Backblaze 的存储桶名 |
| CfAuthKey | 上面创建的 CloudFlare API 令牌 |
| CfHostname | 上面在 CloudFlare 创建的二级域名 |
| CfZoneID | 你的域名的区域 ID |
| CfRulesetID | 上面拿到的规则集 ID |
| CfRuleID | 上面拿到的规则 ID |
然后进入触发器,将路由中的那条记录禁用,因为我们不会用 HTTP 请求来触发这个 Worker。然后再 Cron触发器中添加一个 Cron 触发器。Backblaze 说一个 token 的有效期最大不超过 24 小时,我为了保险起见,选择每半小时就触发这个 Worker 来生成一个新的 token,即 */30 * * * *。
至此前置任务完成,点击右上角的快速编辑,然后将如下脚本粘贴进去,然后点击保存并部署。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
addEventListener("fetch", (event) => { event.respondWith(handleRequest(event.request)); }); addEventListener("scheduled", (event) => { event.waitUntil(updateRule()); }); const getB2Token = async () => { const res = await fetch( "https://api.backblazeb2.com/b2api/v2/b2_authorize_account", { headers: { Authorization: "Basic " + btoa(B2KeyID + ":" + B2AppKey), }, } ); const data = await res.json(); return data.authorizationToken; }; const updateRule = async () => { const b2Token = await getB2Token(); const res = await fetch( `https://api.cloudflare.com/client/v4/zones/${CfZoneID}/rulesets/${CfRulesetID}/rules/${CfRuleID}`, { method: "PATCH", headers: { "Authorization": `Bearer ${CfAuthKey}` }, body: `{ "description": "Replace path for static files for blog", "action": "rewrite", "expression": "(http.host eq \\\"${CfHostname}\\\" and not starts_with(http.request.uri.path, \\\"/file/${B2BucketName}\\\"))", "action_parameters": { "uri": { "path": { "expression": "concat(\\\"/file/${B2BucketName}\\\", http.request.uri.path)" }, "query": { "value": "Authorization=${b2Token}" } } } }`, } ); const data = await res.text(); console.log(data); return data; }; async function handleRequest(request) { const data = await updateRule(); return new Response(data); } |
等 Worker 被触发之后,就可以在浏览器中访问上面配置的域名,来测试到存储桶的连接是否正常。如果测试没问题,就可以把博客中的图片链接换到新地址了。


















