博客图床搭建
阿里云 OSS 搭建图床
优点:
- 国内访问速度快
缺点:
- 需要收费
最最重要的阿里云 OSS 计费有延迟,真遇到别人狂刷流量,当你收到流量异常通知时,通常已经欠了一笔钱了。
免费额度说明:
| 限制项 | 说明 |
|---|---|
| 标准存储 | 本地冗余:0.12 元/GB/月;同城冗余:0.15 元/GB/月 |
| 流量费用 | 外网流出流量:00:00-08:00,0.25 元/GB;08:00-24:00,0.50 元/GB |
那么为了防止被有心之人狂刷流量,可以搭配 CF 带宽联盟实现流量免费
CF 带宽联盟免流原理
带宽联盟是由 CloudFlare 主导组建的旨在节省云计算服务商间数据流量开销的组织,换句话说就是节省 IDC 间的流量传输,减少各 IDC 通过基础 ISP 向互联网传输资源支付的费用。在这个联盟中,受益最大的就是 CF 和末端用户,CF 借此巩固自身的互联网带宽权益地位,更有底气向 ISP 压低带宽价格,用户则获得了来自服务商的流量费用豁免。
带宽联盟官方简介:https://www.cloudflare.com/zh-cn/bandwidth-alliance
阿里云 OSS 已确认除 迪拜 和 中国大陆 地域外流量传出至 CF 边缘节点不计费,计费项目包括存储费用和请求费用。
阿里云官方说明:https://www.aliyun.com/product/news/17749
创建阿里云存储桶
注意,创建存储桶一定一定不要选中国大陆的地域,推荐推荐地域选择 香港。
自定义访问域名(可选)
进入 Bucket 配置 -> 域名管理,配置自定义域名

添加自定义域名 DNS 记录

添加完后再回到阿里云 OSS 域名管理页面,验证域名状态(若验证不成功,关闭小黄云即可验证成功)
注意:完成验证域名状态后一定一定要回到 Cloudflare,开启自定义域名的小黄云代理,这样访问存储桶的流量才会先经过 Cloudflare 的代理,从而实现通过带宽联盟免流。

自定义错误处理页面
- 上传自定义
404.html、index.html到存储桶。 - 进入 数据管理 -> 静态页面,设置默认首页、默认 404 页面。

前置 Worker 代理
设置完私有域名后,出于安全性考虑,存储桶一般都会设置成私有权限,不允许公共读写。
想要访问到相关图片资源只能通过生成临时令牌来实现,预览你存储桶的图片时,你会发现生成的访问 URL 会很长,而且时间过期后 URL 失效,具体如下:
https://test.24hl.cn/test.jpg?Expires=1790245016&OSSAccessKeyId=TMP.3KyX1s6YMUx4NtPpYTWVHx4iobaCdSbafSzYXa7dNB51Bb8C4MbEXHNmBppe68bmMfWGUfjEibSk382L94StazgZiL32wp&Signature=f3qPggCQPlpFzgyiZMGds%2BLNqi0%3D
这样的话,访问起来是非常不方便且不美观的。
概述
通过 Cloudflare Worker 代理自动生成临时访问令牌进行访问

那么这样就只需要通过访问 https://your-worker.workers.dev/test.jpg,Worker 便会自动为你生成临时令牌并返回资源,当资源存在于 Cloudflare 的边缘缓存时,还可以直接访问边缘缓存的资源,而不是重新请求资源。
Aliyun OSS 授权
创建一个 OSS 只读权限的用户,务必保存好 AccessKey ID 、AccessKey Secret。(点击创建)
这个用户是没有上传权限的,因此不能直接在 picGo / picList 中使用,如果要使用,请重新新建一个具有上传权限的用户,尽量最小化权限。

新创建的用户默认是零权限,需要授予权限 OSS 只读权限


创建 Worker
登录 Cloudflare Dashboard,进入 计算 -> Workers & Pages 页面,创建一个 Worker。
进入该 Worker 详情页,进入 设置 -> Runtime variables and secrets -> 添加变量。
- 变量名:
OSS_ACCESS_KEY_ID,值:阿里云 AccessKey ID - 变量名:
OSS_ACCESS_KEY_SECRET,值:阿里云 AccessKey Secret - 变量名:
OSS_BUCKET,值:你的存储名称 - 变量名:
OSS_REGION,值:oss-cn-hongkong

- 变量名:
进入 域 -> 添加域名,绑定自定义域名。
修改自定义代码,重新部署 Worker。
export default {
async fetch(request, env) {
// 仅允许 GET 和 HEAD 请求
if (request.method !== 'GET' && request.method !== 'HEAD') {
return new Response('Method Not Allowed', { status: 405 });
}
const url = new URL(request.url);
// 1. 提取路径:访问 / 时自动映射为 index.html
let objectKey = url.pathname.slice(1);
if (!objectKey || objectKey === '/') {
objectKey = 'index.html';
}
// 防范 URI 编码问题
objectKey = decodeURIComponent(objectKey);
try {
// 2. 尝试向 OSS 请求目标文件
let response = await fetchFromOSS(objectKey, request.method, env);
// 3. 如果文件不存在 (404),尝试请求 404.html
if (response.status === 404) {
const notFoundResponse = await fetchFromOSS('404.html', request.method, env);
if (notFoundResponse.ok) {
const headers = cleanAndPrepareHeaders(notFoundResponse.headers, '404.html');
return new Response(notFoundResponse.body, {
status: 404,
statusText: 'Not Found',
headers: headers
});
}
return new Response('404 Not Found', { status: 404 });
}
// 4. 其他 OSS 错误透传
if (!response.ok) {
const errorText = await response.text();
return new Response(`OSS Error [${response.status}]:\n${errorText}`, {
status: response.status,
headers: { 'Content-Type': 'text/plain;charset=UTF-8' }
});
}
// 5. 成功拿到资源,清理并覆盖 Header 后返回
const responseHeaders = cleanAndPrepareHeaders(response.headers, objectKey);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders
});
} catch (e) {
return new Response(`Worker Internal Error: ${e.message}`, { status: 500 });
}
}
};
// ================= Header 处理逻辑 =================
function cleanAndPrepareHeaders(originalHeaders, objectKey) {
const headers = new Headers(originalHeaders);
// 1. 核心修复:移除强制下载标头,允许浏览器直接渲染 HTML
headers.delete('content-disposition');
// 2. 补全或校准 HTML 的 Content-Type (防止 UTF-8 乱码)
if (objectKey.endsWith('.html') || objectKey.endsWith('.htm')) {
headers.set('Content-Type', 'text/html; charset=UTF-8');
headers.set('Cache-Control', 'public, max-age=3600'); // HTML 页面缓存 1 小时
} else {
// 静态资源(图片、CSS、JS 等)强缓存 1 年
headers.set('Cache-Control', 'public, max-age=31536000, immutable');
}
// 3. 移除暴露的敏感 OSS 标头
headers.delete('x-oss-request-id');
headers.delete('x-oss-server-time');
return headers;
}
// ================= OSS 签名与 Fetch 请求封装 =================
async function fetchFromOSS(objectKey, method, env) {
const expires = Math.floor(Date.now() / 1000) + 60;
const signedOssUrl = await generateV2SignedUrl(
objectKey,
expires,
env.OSS_ACCESS_KEY_ID,
env.OSS_ACCESS_KEY_SECRET,
env.OSS_BUCKET,
env.OSS_REGION
);
return await fetch(signedOssUrl, {
method: method,
headers: {
'User-Agent': 'Cloudflare-Worker-Proxy'
}
});
}
// ================= OSS V2 签名生成核心算法 =================
async function generateV2SignedUrl(objectKey, expires, accessKeyId, accessKeySecret, bucket, region) {
const host = `${bucket}.${region}.aliyuncs.com`;
const canonicalResource = `/${bucket}/${objectKey.startsWith('/') ? objectKey.slice(1) : objectKey}`;
const stringToSign = `GET\n\n\n${expires}\n${canonicalResource}`;
const signature = await hmacSha1Base64(accessKeySecret, stringToSign);
const params = new URLSearchParams();
params.set('OSSAccessKeyId', accessKeyId);
params.set('Expires', expires.toString());
params.set('Signature', signature);
const encodedPath = objectKey.split('/').map(segment => encodeURIComponent(segment)).join('/');
return `https://${host}/${encodedPath}?${params.toString()}`;
}
// ================= Web Crypto 原生加密工具函数 =================
async function hmacSha1Base64(secret, message) {
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const messageData = encoder.encode(message);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-1' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
let binary = '';
const bytes = new Uint8Array(signature);
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
效果展示
index

404

正常访问

Cloudflare R2 搭建图床
cloudflare R2 对象存储,其免费额度每月 10GB 存储和 1000 万次读取,对于日均访问量不大的个人博客通常绰绰有余。
| 限制项 | 免费计划限制 | 说明 |
|---|---|---|
| 存储容量 | 每月 10 GB | 超出后 $0.015/GB/月 |
| A 类操作(写入/更新/删除) | 每月 100 万次 | 超出后 $4.50 / 百万次请求 |
| B 类操作(读取) | 每月 1000 万次 | 超出后 $0.36 / 百万次请求 |
| 出站流量 | 完全免费 | ~ |
创建 R2 存储桶
进入 cloudflare dashboard -> 存储和数据库 -> R2 对象存储,选择 创建存储桶 即可。
前置 Worker 代理
概述
Cloudflare Workers 免费限制是每天 100,000 次请求,当你账户下所有 Worker 产生的请求总数超过次数限制,页面会停止服务,并且返回 429 Too Many Requests。
那么利用这个特性,可以在 R2 图床前置一个 Worker,这样便可以有效防止你的博客被一些别有用心的人爆刷。

免费额度说明:
| 限制项 | 免费计划限制 | 说明 |
|---|---|---|
| 每日请求数 | 100,000 次 / 天 | 所有 Worker 脚本共享此额度,UTC 0 点重置。 |
| CPU 时间 | 10 毫秒 / 请求 | 仅计算 CPU 执行时间,等待网络(如 fetch)的时间不计入。 |
| 子请求 | 50 次 / 请求 | 单个请求内可以发起的子请求(如 fetch)上限。 |
| Worker 数量 | 100 个 | 一个账户最多可以创建 100 个 Worker 脚本。 |
| 环境变量 | 64 个 / Worker | 每个 Worker 可设置的环境变量数量上限。 |
| 内存 | 128 MB | 每次请求可使用的内存上限。 |
| 日志 | 200,000 条 / 天 | 每天可记录的 Workers 日志条数。 |
创建 Worker
登录 Cloudflare Dashboard,进入 计算 -> Workers & Pages 页面,创建一个 Worker。
进入该 Worker 详情页,进入 设置 -> 绑定 -> 添加绑定,添加绑定页面选择 R2 存储桶。
- 变量名称:
MY_BUCKET - R2 存储桶:
your_bucket_name
- 变量名称:
进入 域 -> 添加域名,绑定自定义域名。
修改自定义代码,重新部署 Worker。
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
let path = url.pathname;
// 仅允许 GET 和 HEAD 请求
if (request.method !== 'GET' && request.method !== 'HEAD') {
return new Response('Method Not Allowed', { status: 405 });
}
// ================= 1. 处理首页与路径 =================
// 访问根目录 / 重写为 /static/index.html
if (path === '/') {
path = '/static/index.html';
}
const objectKey = path.slice(1);
try {
// 2. 从 R2 读取对象
const object = await env.MY_BUCKET.get(objectKey);
// 3. 对象存在,正常返回文件
if (object !== null) {
const headers = new Headers();
object.writeHttpMetadata(headers);
// 处理 Content-Type、Cache-Control 与标头清洗
applyHeaderRules(headers, objectKey);
return new Response(request.method === 'HEAD' ? null : object.body, { headers });
}
// 4. 读取并返回自定义 404 页面
const notFoundPage = await env.MY_BUCKET.get('static/404.html');
if (notFoundPage !== null) {
const headers = new Headers();
notFoundPage.writeHttpMetadata(headers);
headers.set('Content-Type', 'text/html; charset=UTF-8');
headers.set('Cache-Control', 'no-cache');
return new Response(request.method === 'HEAD' ? null : notFoundPage.body, {
status: 404,
headers
});
}
return new Response('404 Not Found', { status: 404 });
} catch (e) {
return new Response(`Worker Internal Error: ${e.message}`, { status: 500 });
}
},
};
// ================= Header 处理逻辑 =================
function applyHeaderRules(headers, objectKey) {
headers.delete('content-disposition');
const ext = objectKey.split('.').pop().toLowerCase();
const mimeTypes = {
'html': 'text/html; charset=UTF-8',
'htm': 'text/html; charset=UTF-8',
'css': 'text/css; charset=UTF-8',
'js': 'application/javascript; charset=UTF-8',
'json': 'application/json; charset=UTF-8',
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'webp': 'image/webp',
'svg': 'image/svg+xml',
'ico': 'image/x-icon'
};
if (mimeTypes[ext]) {
headers.set('Content-Type', mimeTypes[ext]);
}
if (ext === 'html' || ext === 'htm') {
headers.set('Cache-Control', 'public, max-age=3600');
} else {
headers.set('Cache-Control', 'public, max-age=31536000, immutable');
}
headers.set('X-Content-Type-Options', 'nosniff');
}
创建自定义页面
进入 Cloudflare Dashboard -> 存储和数据库 -> R2 对象存储,进入你的存储桶。
在存储桶根目录下,创建一个名为
static的文件夹。上传自定义
index.html和404.html,上传到static文件夹内。index.html
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome</title>
<style>
:root {
--bg-color: #121212;
--text-main: #e0e0e0;
--text-sub: #a0a0a0;
--accent: #3b82f6;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background-color: var(--bg-color);
color: var(--text-main);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 20px;
}
h1 {
font-size: 2.5rem;
font-weight: 600;
margin-bottom: 16px;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
p {
color: var(--text-sub);
font-size: 1.1rem;
line-height: 1.6;
max-width: 500px;
}
</style>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>404.html
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>404 - 页面未找到</title>
<style>
:root {
--bg-color: #121212;
--card-bg: #1e1e1e;
--text-main: #e0e0e0;
--text-sub: #a0a0a0;
--accent: #3b82f6;
--accent-hover: #2563eb;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--bg-color);
color: var(--text-main);
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background-color: var(--card-bg);
padding: 40px 30px;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
text-align: center;
max-width: 400px;
width: 100%;
border: 1px solid #2a2a2a;
}
.error-code {
font-size: 6rem;
font-weight: 800;
line-height: 1;
margin-bottom: 20px;
background: linear-gradient(135deg, #f87171, #ef4444);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
h1 {
font-size: 1.5rem;
margin-bottom: 12px;
color: var(--text-main);
}
p {
color: var(--text-sub);
font-size: 0.95rem;
line-height: 1.6;
margin-bottom: 30px;
}
.btn {
display: inline-block;
background-color: var(--accent);
color: #ffffff;
text-decoration: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: 500;
transition:
background-color 0.2s ease,
transform 0.1s ease;
}
.btn:hover {
background-color: var(--accent-hover);
}
.btn:active {
transform: scale(0.97);
}
</style>
</head>
<body>
<div class="container">
<div class="error-code">404</div>
<h1>页面未找到</h1>
<p>抱歉,您访问的内容不存在或已被移除。</p>
<a href="/" class="btn">返回首页</a>
</div>
</body>
</html>
效果展示
- 自定义 index

- 自定义 404

设置缓存规则
进入 域名详情页 -> 规则 -> 概述,选择 创建规则 -> 缓存规则。
规则名称:随意

传入请求匹配:自定义筛选表达式
(http.request.uri.path.extension in {"jpg" "jpeg" "mkv" "mp3" "mp4" "png" "svg" "swf" "webp" "woff" "woff2" "gif" "icon" "ico"})

缓存资格:符合缓存条件

边缘 TTL:1 天

浏览器 TTL:12 小时

创建访问密钥(配合 picList 使用)
进入 存储与数据库 -> R2 对象存储,选择右下角的 管理 API 令牌,选择 创建 Account API 令牌
- 令牌名称:随意
- 权限:对象读和写
- 指定存储桶:选择你自己的存储桶(最小权限原则)
- TTL:永久
创建好后记住以下值(只会显示一次,请务必记住)
S3_ACCESS_KEY_ID(访问密钥 ID)
S3_SECRET_ACCESS_KEY(机密访问密钥)
S3_ENDPOINT(为 S3 客户端使用管辖权地特定的终结点)

最后进入 picList / picGo 配置好,就能愉快使用你的图床啦。


