NOTE

什么是图床

图床就是将图片上传到相关服务商或者个人服务器,通过上传文件的网络地址进行远程访问,可以方便快速的将图片插入到文章中,方便后续图片二次使用、迁移、分享。

阿里云 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 配置 -> 域名管理,配置自定义域名

PixPin_2026-09-24_15-55-24

添加自定义域名 DNS 记录

PixPin_2026-09-24_15-59-26

添加完后再回到阿里云 OSS 域名管理页面,验证域名状态(若验证不成功,关闭小黄云即可验证成功)

注意:完成验证域名状态后一定一定要回到 Cloudflare,开启自定义域名的小黄云代理,这样访问存储桶的流量才会先经过 Cloudflare 的代理,从而实现通过带宽联盟免流。

PixPin_2026-09-24_16-18-46

自定义错误处理页面

  1. 上传自定义 404.html、index.html 到存储桶。
  2. 进入 数据管理 -> 静态页面,设置默认首页、默认 404 页面。
    PixPin_2026-09-24_18-24-42

前置 Worker 代理

设置完私有域名后,出于安全性考虑,存储桶一般都会设置成私有权限,不允许公共读写。

想要访问到相关图片资源只能通过生成临时令牌来实现,预览你存储桶的图片时,你会发现生成的访问 URL 会很长,而且时间过期后 URL 失效,具体如下:

https://test.24hl.cn/test.jpg?Expires=1790245016&OSSAccessKeyId=TMP.3KyX1s6YMUx4NtPpYTWVHx4iobaCdSbafSzYXa7dNB51Bb8C4MbEXHNmBppe68bmMfWGUfjEibSk382L94StazgZiL32wp&Signature=f3qPggCQPlpFzgyiZMGds%2BLNqi0%3D

这样的话,访问起来是非常不方便且不美观的。

概述

通过 Cloudflare Worker 代理自动生成临时访问令牌进行访问

PixPin_2026-09-24_18-37-30

那么这样就只需要通过访问 https://your-worker.workers.dev/test.jpg,Worker 便会自动为你生成临时令牌并返回资源,当资源存在于 Cloudflare 的边缘缓存时,还可以直接访问边缘缓存的资源,而不是重新请求资源。

Aliyun OSS 授权

创建一个 OSS 只读权限的用户,务必保存好 AccessKey ID 、AccessKey Secret。(点击创建)

这个用户是没有上传权限的,因此不能直接在 picGo / picList 中使用,如果要使用,请重新新建一个具有上传权限的用户,尽量最小化权限。

PixPin_2026-09-24_16-49-22

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

PixPin_2026-09-24_16-53-08

PixPin_2026-09-24_16-51-48

创建 Worker

  1. 登录 Cloudflare Dashboard,进入 计算 -> Workers & Pages 页面,创建一个 Worker。

  2. 进入该 Worker 详情页,进入 设置 -> Runtime variables and secrets -> 添加变量。

    • 变量名:OSS_ACCESS_KEY_ID,值:阿里云 AccessKey ID
    • 变量名:OSS_ACCESS_KEY_SECRET,值:阿里云 AccessKey Secret
    • 变量名: OSS_BUCKET,值:你的存储名称
    • 变量名:OSS_REGION,值:oss-cn-hongkong

    PixPin_2026-09-24_18-53-01

  3. 进入 域 -> 添加域名,绑定自定义域名。

  4. 修改自定义代码,重新部署 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
    PixPin_2026-09-24_19-01-05

  • 404
    PixPin_2026-09-24_19-09-21

  • 正常访问
    PixPin_2026-09-24_19-10-10

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,这样便可以有效防止你的博客被一些别有用心的人爆刷。

PixPin_2026-09-24_14-00-37

免费额度说明:

限制项 免费计划限制 说明
每日请求数 100,000 次 / 天 所有 Worker 脚本共享此额度,UTC 0 点重置。
CPU 时间 10 毫秒 / 请求 仅计算 CPU 执行时间,等待网络(如 fetch)的时间不计入。
子请求 50 次 / 请求 单个请求内可以发起的子请求(如 fetch)上限。
Worker 数量 100 个 一个账户最多可以创建 100 个 Worker 脚本。
环境变量 64 个 / Worker 每个 Worker 可设置的环境变量数量上限。
内存 128 MB 每次请求可使用的内存上限。
日志 200,000 条 / 天 每天可记录的 Workers 日志条数。

创建 Worker

  1. 登录 Cloudflare Dashboard,进入 计算 -> Workers & Pages 页面,创建一个 Worker。

  2. 进入该 Worker 详情页,进入 设置 -> 绑定 -> 添加绑定,添加绑定页面选择 R2 存储桶。

    • 变量名称: MY_BUCKET
    • R2 存储桶: your_bucket_name
  3. 进入 域 -> 添加域名,绑定自定义域名。

  4. 修改自定义代码,重新部署 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');
    }

创建自定义页面

  1. 进入 Cloudflare Dashboard -> 存储和数据库 -> R2 对象存储,进入你的存储桶。

  2. 在存储桶根目录下,创建一个名为 static 的文件夹。

  3. 上传自定义 index.html 和 404.html,上传到 static 文件夹内。

    • index.html
    <!DOCTYPE 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
    <!doctype 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
    PixPin_2026-09-24_14-29-37
  • 自定义 404
    PixPin_2026-09-26_13-49-48

设置缓存规则

进入 域名详情页 -> 规则 -> 概述,选择 创建规则 -> 缓存规则。

  • 规则名称:随意
    PixPin_2026-09-24_14-53-22

  • 传入请求匹配:自定义筛选表达式

    (http.request.uri.path.extension in {"jpg" "jpeg" "mkv" "mp3" "mp4" "png" "svg" "swf" "webp" "woff" "woff2" "gif" "icon" "ico"})

    PixPin_2026-09-24_14-54-02

  • 缓存资格:符合缓存条件
    PixPin_2026-09-24_14-54-13

  • 边缘 TTL:1 天
    PixPin_2026-09-24_14-54-32

  • 浏览器 TTL:12 小时
    PixPin_2026-09-24_14-54-43

创建访问密钥(配合 picList 使用)

进入 存储与数据库 -> R2 对象存储,选择右下角的 管理 API 令牌,选择 创建 Account API 令牌

  • 令牌名称:随意
  • 权限:对象读和写
  • 指定存储桶:选择你自己的存储桶(最小权限原则)
  • TTL:永久

创建好后记住以下值(只会显示一次,请务必记住)

  • S3_ACCESS_KEY_ID(访问密钥 ID)

  • S3_SECRET_ACCESS_KEY(机密访问密钥)

  • S3_ENDPOINT(为 S3 客户端使用管辖权地特定的终结点)

PixPin_2026-09-24_15-09-25

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