使用 Cloudflare Workers 构建免费图床:控制 R2 成本并使用缓存

使用 Cloudflare Workers 构建免费图床:控制 R2 成本并使用缓存

tsvico Lv5

Cloudflare R2 确实是个好东西,10GB 免费空间加上每月一千万次免费 Class B 操作,比某 AWS 不知道高到哪里去了。但是有个坑:公开存储桶的请求次数是不设防的。万一有人恶意刷你的桶,或者你的资源突然火了被疯狂访问,账单能直接让你破产。

如何用 Cloudflare Worker 给 R2 图床加个安全阀

解决方案

其实原理很简单:用 Worker 当中间人。把存储桶设为私有,然后通过 Worker 来代理访问。这样有两个好处:

  1. Worker 免费版每天 10 万次请求限制成了天然防火墙
  2. 还能顺手加个缓存,减少实际访问 R2 的次数

具体操作

第一步:创建 R2 存储桶

  1. 进 Cloudflare 控制台
  2. 找到 R2 页面
  3. 点” 创建存储桶”
  4. 起个名字(比如 my-private-bucket
  5. 重要:别勾选” 公开访问”

第二步:创建 Worker

参考地址 使用 Cloudflare Worker 的免费账户限制 R2 的支出(新版本更新使用 D1 数据库限制次数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
async fetch(request, env) {
const path = decodeURIComponent(new URL(request.url).pathname.slice(1));

if (!path) {
return new Response("Not Found", { status: 404 });
}

try {
const file = await env.OPCT.get(path);

if (!file) {
return new Response("Not Found", { status: 404 });
}

const headers = new Headers();
headers.set('Content-Type', file.httpMetadata?.contentType || 'application/octet-stream');

return new Response(file.body, { headers });
} catch (error) {
return new Response("Server Error", { status: 500 });
}
}
}

第三步:绑定资源

  1. 在 Worker 设置里找到” 资源绑定”
  2. 添加新绑定:
    • 变量名:OPCT(随便起,但代码里要用一样的)
    • 类型:R2 存储桶
    • 选择你刚创建的存储桶

注意事项

  1. Worker 位置:保持默认就行,选” 智能路由” 反而可能把请求都路由到存储桶所在区域

为什么需要缓存

最开始我用的无缓存版本,在讨论帖我使用 CloudflareWorker 做地球图片后端,我试图使用 Cloudflare 缓存但 cf-cache-status 字段缺失没有缓存中发现我的图床也一样,响应头里没有 cf-cache-status,说明完全没走缓存。查了文档才知道要用 caches.default 这个 API。加了缓存之后:

  1. 重复请求不会打到 R2
  2. 响应速度更快
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
export default {
async fetch(request, env) {
const path = decodeURIComponent(new URL(request.url).pathname.slice(1));

if (!path) {
return new Response("Not Found", { status: 404 });
}

// Get cache instance
const cache = caches.default;

try {
// First try to get from cache
let response = await cache.match(request);

if (response) {
return response; // Return cached response if found
}

// If not in cache, get from OPCT
const file = await env.OPCT.get(path);

if (!file) {
return new Response("Not Found", { status: 404 });
}

// Create response with proper headers
const headers = new Headers();
headers.set('Content-Type', file.httpMetadata?.contentType || 'application/octet-stream');
// Set cache control headers (adjust TTL as needed)
headers.set('Cache-Control', 'public, max-age=3600'); // 1 hour cache [^1]

response = new Response(file.body, { headers });

// Store in cache for future requests
await cache.put(request, response.clone()); // [^2]

return response;
} catch (error) {
return new Response("Server Error", { status: 500 });
}
}
}

其他方案

看到有人用 D1 数据库做访问次数限制 使用 Cloudflare Worker 的免费账户限制 R2 的支出(新版本更新使用 D1 数据库限制次数) ,适合更精细的控制。但我觉得对于普通用途,Worker 的每日 10 万次限制 + 缓存已经够用了

进一步优化缓存 (20250805)

  • 设置响应头
    1
    headers.set('Cache-Control', 'public, max-age=2592000');
  • 缓存键根据 URL
    1
    2
    const cacheKey = new Request(request.url, { method: "GET" });
    let response = await cache.match(cacheKey);
  • 非 get 不响应
    1
    2
    3
    if (request.method !== 'GET') {
    return new Response("Method Not Allowed", { status: 405 });
    }
    完整版代码
    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
    export default {
    async fetch(request, env) {
    // 仅允许 GET 请求
    if (request.method !== 'GET') {
    return new Response("Method Not Allowed", { status: 405 });
    }

    // 获取请求路径(忽略 query 参数)
    const url = new URL(request.url);
    const path = decodeURIComponent(url.pathname.slice(1)); // 去掉开头的 '/'

    if (!path) {
    return new Response("Not Found", { status: 404 });
    }

    // 获取默认缓存实例
    const cache = caches.default;

    // 使用忽略 query 参数的 URL 构建缓存 key
    const cacheKeyUrl = `${url.origin}${url.pathname}`; // 不包含 search/query
    const cacheKey = new Request(cacheKeyUrl, { method: "GET" });

    try {
    // 先尝试从缓存获取响应
    let response = await cache.match(cacheKey);

    if (response) {
    return response; // 缓存命中,直接返回
    }

    // 如果缓存未命中,则从 KV 存储中获取数据
    const file = await env.OPCT.get(path);

    if (!file) {
    return new Response("Not Found", { status: 404 });
    }

    // 设置响应头
    const headers = new Headers();
    headers.set('Content-Type', file.httpMetadata?.contentType || 'application/octet-stream');

    // 设置缓存控制头,客户端也可缓存 1 个月
    headers.set('Cache-Control', 'public, max-age=2592000'); // 1 个月缓存

    // 构造响应对象
    response = new Response(file.body, { headers });

    // 将响应缓存以供后续请求使用
    await cache.put(cacheKey, response.clone());

    return response;
    } catch (error) {
    return new Response("Server Error", { status: 500 });
    }
    }
    }

参考链接

  1. 使用 Cloudflare Worker 的免费账户限制 R2 的支出(新版本更新使用 D1 数据库限制次数)
  2. 我使用 CloudflareWorker 做地球图片后端,我试图使用 Cloudflare 缓存但 cf-cache-status 字段缺失没有缓存
  3. 使用 Cloudflare Workers 中的 Cache API 来全局缓存,减少 KV 的读写,可跨 worker 缓存
  • 标题: 使用 Cloudflare Workers 构建免费图床:控制 R2 成本并使用缓存
  • 作者: tsvico
  • 创建于 : 2025-04-09 18:20:40
  • 更新于 : 2025-08-05 22:25:41
  • 链接: https://blog.tbox.fun/2025/2622120444.html
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论