前言

目前我的博客使用 hexo 搭建,写作是使用 Markdown,博客的原始文件被推送到 gitlab 仓库后,Gitlab-CI 会自动从 Markdown 生成静态文件,然后部署静态文件到 Gitlab Pages。Markdown 在易于写作(比如:代码块、插入链接、表格)的同时,也带来了一个不方便的问题,就是插入图片,毕竟 Markdown 不像 doc 那样,图片直接插入到文档中,Markdown 插入图片是以链接的形式。

如果将图片直接放在子目录中,插入图片使用子目录的链接,加载的时候可能会很慢,相当于加载 gitlab 仓库的 raw 流,一般的方案是将图片存放在一个专门的服务器上,这样的服务器通常被称为 图床

这里插一句,为什么图床能够使加载变快呢?

一个原因是,浏览器在加载网页时,对同一域名的并发连接数有限制,超过限制的请求会进入队列,Chrome/Firefox 是 6,Safari 是 4,而图床的域名和访问网址的域名不一样(要是一样,那不是子文件夹么…)。

另一个原因是,图床一般会使用 CDN 网络,用户会连接到离他最近的 CDN 节点,加快获取图片的内容。

我观察了一些博客是如何加载图片的:

  • google storage
    cloudflare blog 就是这样做的,但是有时候也放在子目录中
  • 阿里 CDN (alicdn)
    需要备案,且套路云 pass
  • yecdn.com
    不知道是哪个大佬的备案域名,提供了 CNAME 方式,可以以此接入 七牛CDN 等需要备案的 CDN
  • i.loli.net
    不用备案,兽兽土豪提供的免费图床,Anycast 网络
  • 七牛 CDN (qiniucdn)
    需要备案,pass
  • Github 图床
    emmm,算了吧,也不快
  • Cloudfront.net
    AWS 的 CDN,国内是被墙的状态
  • weibo 图床
    可以查到上传者的微博账号,不推荐使用

我最开始使用 sm.ms 的免费图床,他家图床免费使用,提供 anycast 节点,兽兽大佬很慷慨,但是作为个人来说,管理不方便,删除图片需要提供删除链接(删除链接是上传图片时给出),终究不是个长久之计。

后来我切换到了 cloudinary,免费额度足够一般的博客使用,可以上传 照片/视频,使用的是 Akamai 的 CDN(CCTV海外站用的就是 Akamai) ,国内访问友好,云端上有强大的图像处理功能,但是我一直想有一个 CNAME 到自己域名的链接,他家的这个功能是收费的,如果价格便宜(5刀以内/月)还是可以支持一下,关键是价格也不便宜,$224/mo(参见:https://cloudinary.com/pricing )。

直到今天,看到 科技爱好者周刊:第 74 期 中分享的 使用 Backblaze B2 和 Cloudflare Workers 搭建免费图床 一文,是我心中想要的图床方案。

我照着这篇教程踩了一遍坑,顺便根据我的个人环境,添加了一些配置,现在记录如下。

各个组件的介绍

Backblaze B2

Backblaze B2 是一个云存储解决方案,类似于Amazon AWS S3, 但是价格稍微便宜一些. 前 10GB 存储是完全免费的.

通常情况下,像使用 AWS S3 这类的服务,你必须为所提供的内容支付带宽费用,这通常是费用中最高的部分。不过,由于有了 Bandwidth Alliance(带宽联盟),Backblaze 到 Cloudflare 之间的出口是完全免费的,我们将大量使用这一优势。

Dropshare

Dropshare 是一个 macOS 软件,可以用于截图、录制屏幕视频、上传第三方云存储等等.

和原博客介绍的 ShareX 有着相似的功能,可以集成 Backblaze,并且支持 CNAME 域名,因为上传了文件后,你肯定需要拷贝图片的链接,并将其粘贴到 Markdown 或者其他的地方。

如果你使用的是其他的平台,可以参考以下两条链接:

  1. https://help.backblaze.com/hc/en-us/articles/226688888-How-can-I-upload-files-to-B2-
  2. https://www.backblaze.com/b2/integrations.html?platform=mac

另外还有个工具,叫 uPic,似乎专门为了上床图床而生,此工具集成了众多图床,还支持自定义 API,虽然原生并未支持 backblaze,但是我不确定 自定义 API 的功能是否能够良好支持 backblaze.

Cloudflare

我总是很难描述 Cloudflare 的确切功能,但是按照他们的网站,它们是“世界上最大的网络之一”。 如今,企业、非营利组织、博客作者以及拥有互联网存在的任何人都拥有更快,更安全的网站和应用程序。” 在本教程中,我们将使用他们的 CDN 、DNS、以及 Cloudflare Workers,通过重写 URL 来增加炫酷感。

Cloudflare dashboard

免费额度

教程开始前,先罗列一下用到的各个组件的免费额度.

cloudflare workers

ref: https://www.cloudflare.com/products/cloudflare-workers/

  • 100,000 requests per day.
    一天 10万次请求
  • Up to 10ms CPU time per request.
    每次请求最大占用 CPU 工作时间为 10ms (忽略)

Backblaze Storage 存储

ref: https://www.backblaze.com/b2/cloud-storage-pricing.html

  • first 10 GB for free.
    前 10GB 存储免费

Backblaze Download 下载

ref: https://www.backblaze.com/b2/cloud-storage-pricing.html

  • The first 1 GB of data downloaded each day is free.
    每天前 1GB 下载免费

因为有 Bandwidth Alliance(带宽联盟),这个限制我们可以忽略不计。

Backblaze Transactions API Calls(API调用)

ref:

  1. https://www.backblaze.com/b2/cloud-storage-pricing.html
  2. https://www.backblaze.com/b2/b2-transactions-price.html
  • Class “B” transactions - $0.004 per 10,000 with 2,500 free per day.
    B 类传输,每天前 2500 次免费

cloudflare workers 需要用到的 b2_download_file_by_name,属于 B 类传输。

Backblaze B2 提供了 10G 的免费存储空间,并且搭配 Cloudflare 的 page rules 来配置一些缓存规则,对于个人用户绰绰有余.

Backblaze 当前的配额使用量可以在面板的 “Caps & Alerts” 查看,修改配额会要求提供信用卡,以便用于付费。所以,在你提供信用卡之前,一切都是免费的。

教程开始

你需要有一个域名,选择一个子域名作为图床的域名。

关于图床的二级域名命名,一般有以下几种:

  • img
  • images
  • image
  • assets (不仅存放图片,还有存放 css、js)
  • static (不仅存放图片,还有存放 css、js)

我这里采用第一种。

如果你是土豪,还可以专门注册一个域名用于存放图片。
比如, pixiv 就是这样做的,pixiv 的图床是 i.pximg.net 。
而,gitlab 则是组合了上面列举的两种,assets.gitlab-static.net。

创建 Backblaze B2 Bucket(存储桶)

首先,你需要在 Backblaze’s B2 storage 注册一个账号,注册好了后,在后台面板选择 “Buckets”(存储桶),然后点击 “Create a Bucket”,创建一个存储桶。

创建一个 Backblaze B2 存储桶

这里不得不说,backblaze 的 Web 端 cookie 有效时间比较短,一会儿会话就过期了。(小声bb)

存储桶的名字是全局唯一的,所以你需要想一个特殊的名字。请注意,要确保存储桶的类型是public(公开)的,因为我们想要访客可以看到我们上传的图片。

存储桶创建好后,跳转到 “App keys(应用程序密钥)” 部分,点击 “Add a New Application Key(创建一个应用程序密钥)”,这是为了让 Dropshare 这样的第三方软件接入使用 backblaze,用于上传图片。

添加 Backblaze B2 Cloud Storage 的应用程序密钥

给你的密钥取个名字,比如 dropshare ,并且我建议,限制仅可访问你之前创建的存储桶。这并不是必需的操作,但是是一个为了安全的良好实践。接入的类型是 “Read and Write(可读写)”, 并且不要填写 文件名前缀, 或者是密钥的有效时间,因为一旦填写,密钥将在某一天过期,无法永久可用.

Backblaze B2 应用程序密钥创建成功

一旦你的密钥创建成功, 在安全的地方,比如密码管理器,记录下 keyID 和 applicationKey. 因为当你关闭提示后,你将无法再次获取到 applicationKey, 所以,确保要保存好,如果没了,就删了重新创建就行了.

最后,回到 “Buckets(存储桶)” , 在你刚刚创建的存储桶上,点击 “Upload/Download(上传/下载)”. 上传一个临时的测试文件 (比如 test.txt)到存储桶中,然后在你刚上传的文件处,点击 “i(information,信息)”. 这是为了获取下一步需要的信息,你的存储桶处于哪台服务器上.

在 Backblaze B2 Bucket 上传文件后的信息示例

在上面的截图中,可以看到,我的文件是放在 https://f000.backblazeb2.com/ - 记住这个域名,等会儿我们会有用.

Cloudflare DNS

接下来, 我们来到 Cloudflare , 登陆你的账号,如果没有账号,那就创建一个. 在开始前,你需要一个有效的域名, 跟着教程,将域名接入到他们的平台中.

域名接入完成后,在你的域名里面, 创建一条 CNAME 类型的 DNS 记录,指向之前发现的 fxxx.backblazeb2.com 域名.

为 Backblaze B2 创建的 Cloudflare DNS 记录

根据上面截图中的记录, 我使用这个子域名 img.meow.page 作为图床的域名, 并且指向 f000.backblazeb2.com. 确保 Cloudflare 橙色保护盾是开启的状态, 这代表请求是通过了 Cloudflare 的 CDN 代理层. Cloudflare 默认的 TTL 将被设置为 auto(自动),对于我们的使用已经足够.

Backblaze 在他们的网站上有一篇 帮助文档 讲述了更多有关配置 Cloudflare 和 Backblaze B2 的技巧, 以及一些有关创建页面规则来确保您的域名只能用于从存储桶中获取文件的说明.

在将其部署到生产环境中之前,建议您遵循以下说明.

如果你的文件不会经常改变,我强烈建议你,添加一条 page-rule(页面规则) 来设置 “cache level(缓存等级)” 为 “everything(所有)”, 并且 “edge cache TTL(边界缓存存活时间)” 设置为较高的值,比如 7天

测试一下,如果通过 https://subdomain.domain.com/file/<bucket-name>/test.txt 这样的链接,你能访问之前上传的文件. 说明你的配置成功了!

Dropshare 配置

接下来的步骤是帮助你如何利用 Dropshare 来上传文件到 B2 Bucket 存储桶.

安装完 Dropshare 后, 看起来有蛮多的项目需要设置.

首先, 我们来配置一个 Backblaze B2 连接. 连接,是告诉 Dropshare ,你想把文件上传到哪。

在 macOS 的状态栏,找到 Dropshare 的后台图标,点开他的界面,然后点击⚙️齿轮图标,你会看到一列菜单,点击 “Preference(设置)”,你会进入设置界面。在设置窗口中, 依次点击 “Connections(连接) -> New Connection(添加新的连接)”.

Dropshare 设置界面

在弹出的窗口上,选择 第三方云存储(Third-Party Cloud) 区域中的 B2 图标,我已经用框画出。点击 B2 后,你会看到这样的窗口。

设置 Backblaze 连接

按照说明,将存储桶的名字,以及之前生成的 Application Key ID 和 Application Key(即secret) 填上去。

Cloudflare Workers

通常来说, 现在这样,一切都可以正常工作了,如果你只要一个基础的图片存储,那么,你不需要配置其他的东西. 但是,为了一些其他的酷炫效果,我们可以使用 Cloudflare Workers 来重写图片的 URL 使链接更加友好, 比如从 URL 中去掉无用的 /file/<bucket-name>/ 部分. 举个例子, https://subdomain.domain.com/file//test.txt 链接将会变成 https://subdomain.domain.com/test.txt.

Cloudflare Workers 允许你在 Cloudflare 全球网络中的边界服务器运行 JavaScript 代码, 使开发者可以部署 serverless(无需服务器) 的应用程序,并且自动缩放. 对于部署的 worker , 以下是我们想要达到的目标:

  • 去除 URL 中的 /file/<bucket-name> 部分
  • 去除一些从 Backblaze B2 响应的无用请求头
  • 加上基本的 CORS 请求头,以便允许图片嵌入到网站中
  • 为图片优化缓存 (浏览器的缓存, 以及 CDN 边界服务器上的缓存)

在 Cloudflare 你的域名处, 创建一个新的 worker 脚本.

以下代码是我在 worker 中使用的(感谢原作者,他的脚本利用的 Cloudflare 边界服务器的缓存,使得请求不再发送到 Backblaze 去获取图片文件,我在他的脚本基础上进行修改)。

使用前,注意修改 b2Domainb2Bucket 这两个变量的值.
b2Domain,是你图床的二级域名。
b2Bucket,是你的 bucket 存储桶的名字。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
'use strict';
const b2Domain = 'img.domain.com'; // configure this as per instructions above
const b2Bucket = 'bucket-name'; // configure this as per instructions above
const b2UrlPath = `/file/${b2Bucket}/`;
addEventListener('fetch', event => {
return event.respondWith(fileReq(event));
});

// define the file extensions we wish to add basic access control headers to
const corsFileTypes = ['png', 'jpg', 'gif', 'jpeg', 'webp'];

// backblaze returns some additional headers that are useful for debugging, but unnecessary in production. We can remove these to save some size
const removeHeaders = [
'x-bz-content-sha1',
'x-bz-file-id',
'x-bz-file-name',
'x-bz-info-src_last_modified_millis',
'X-Bz-Upload-Timestamp',
'Expires'
];
const expiration = 31536000; // override browser cache for images - 1 year

// define a function we can re-use to fix headers
const fixHeaders = function(url, status, headers){
let newHdrs = new Headers(headers);
// add basic cors headers for images
if(corsFileTypes.includes(url.pathname.split('.').pop())){
newHdrs.set('Access-Control-Allow-Origin', '*');
}
// override browser cache for files when 200
if(status === 200){
newHdrs.set('Cache-Control', "public, max-age=" + expiration);
}else{
// only cache other things for 5 minutes
newHdrs.set('Cache-Control', 'public, max-age=300');
}
// set ETag for efficient caching where possible
const ETag = newHdrs.get('x-bz-content-sha1') || newHdrs.get('x-bz-info-src_last_modified_millis') || newHdrs.get('x-bz-file-id');
if(ETag){
newHdrs.set('ETag', ETag);
}
// remove unnecessary headers
removeHeaders.forEach(header => {
newHdrs.delete(header);
});
return newHdrs;
};
async function fileReq(event){
const cache = caches.default; // Cloudflare edge caching
const url = new URL(event.request.url);
if(url.host === b2Domain && !url.pathname.startsWith(b2UrlPath)){
url.pathname = b2UrlPath + url.pathname;
}
let response = await cache.match(url); // try to find match for this request in the edge cache
if(response){
// use cache found on Cloudflare edge. Set X-Worker-Cache header for helpful debug
let newHdrs = fixHeaders(url, response.status, response.headers);
newHdrs.set('X-Worker-Cache', "true");
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHdrs
});
}
// no cache, fetch image, apply Cloudflare lossless compression
response = await fetch(url, {cf: {polish: "lossless"}});
let newHdrs = fixHeaders(url, response.status, response.headers);

if(response.status === 200){

response = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHdrs
});
}else{
response = new Response('File not found!', { status: 404 })
}

event.waitUntil(cache.put(url, response.clone()));
return response;
}

你需要进入 workers 功能,点击 Launch Editor ,进入编辑器,将以上代码拷贝到编辑器中,然后保存 worker。

使用了这个 worker 后, 你可以从 URL 中去掉 /file/<bucket-name>/ 部分, 使得生成的 URL 是这样的形式 https://subdomain.domain.com/test.txt, 而不是 https://subdomain.domain.com/file/<bucket-name>/test.txt.

你需要添加一条 worker 的路由规则,使访问 subdomain.domain.com/*时,请求先由 worker 来处理。

为 子域名设置 worker 路由规则

然后,你可以在 Dropshare 里面配置 CNAME,当你上传图片后,点击复制 URL,此时的 URL 将会是你的 二级域名的 URL。( Dropshare 竟然支持了这个设置,非常良心了啊)

在 Dropshare 中,进入设置里面的 Connections, 当鼠标悬浮在刚刚配置好的连接上,右边有个下拉的倒三角形箭头,点以下箭头,会出现 edit 选项,你可以在这里修改刚刚的连接。

为 Dropshare 设置 Domain Alias

在 Domain Alias 一栏填入你的图床域名,然后保存。

后续

访问不带文件名的 URL 时会报错

1
2
3
4
5
{
"code": "bad_request",
"message": "File names must contain at least one character",
"status": 400
}

使用另一个 worker,定义一条 route 即可解决,比如写个 helloworld 的 worker:

1
2
3
4
5
6
7
8
9
10
11
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})

/**
* Fetch and log a request
* @param {Request} request
*/
async function handleRequest(request) {
return new Response('Hello!', { status: 200 })
}

定义的 route:

Route Worker
subdomain.domain.com helloworld

防盗链

这个问题,我曾和原作者沟通过,为了防止图片被别人盗取直接使用。
因为我在翻译 Cloudflare 博客文章的时候就是这样干的(逃~~~

解决方案:
简单地通过判断 referer 来解决吧。
在 Cloudflare 的 Firewall 功能中,添加一条 firewall rule.

这条规则的意思是:

http.request.full_uri contains “https://img.meow.page"
如果完整的请求(URI Full)中包含我的图床的域名(https://img.meow.page)

and
并且

not http.referer contains “blog.meow.page”
请求头中的 Referer,没有包含我的博客地址(blog.meow.page)
也就是说请求不是从我的博客来的
不是因为访问我的博客而加载的图片

action block
这样的请求会被 block

防止暴力枚举

为了防止恶意访客耗费 API Call 额度,我修改来原 worker 脚本,重写了错误页面的返回信息。

如果用户请求了错误的文件,那么页面会报错,并且将 bucket-name 暴露出来。

1
2
3
4
5
{
"code": "not_found",
"message": "bucket {bucket-name} does not have file: {file-name}",
"status": 404
}

恶意的访问者,可能会直接用这样的链接去消耗 API Call 额度。

1
https://f00x.backblazeb2.com/file/{bucket-name}/1.jpg

whatever

emmmmmm,其实 referer 也是很容易欺骗的,你可以欺骗了referer后,照样耗费 API Call 额度,
这只是取决于人的道德吧。

感觉我写了这么多防御,好像是给恶意访问者提供了攻击思路一样?

但是不写出来,并不代表这些问题就会被掩盖。

参考:

  1. https://jross.me/free-personal-image-hosting-with-backblaze-b2-and-cloudflare-workers/
  2. https://help.backblaze.com/hc/en-us/articles/217666928-Using-Backblaze-B2-with-the-Cloudflare-CDN
  3. https://help.backblaze.com/hc/en-us/articles/226688888-How-can-I-upload-files-to-B2-
  4. https://www.alibabacloud.com/help/zh/doc-detail/31937.htm
  5. https://www.alibabacloud.com/help/zh/doc-detail/31928.htm