Cloudflare Workers 踩坑筆記:適用場景、設定,以及怎麼安全地結合 R2

從一次「桌面 app 要把資料寫進 R2」的實戰出發,整理 Cloudflare Workers 的適用場景、設定步驟,以及結合 R2 時最容易踩的幾個坑——尤其是「絕對不要把 R2 金鑰塞進 client」。

Cloudflare Workers 踩坑筆記:適用場景、設定,以及怎麼安全地結合 R2

這篇的起點是一個很具體的需求:我有一個會發到很多台電腦的桌面 app,想在偵測到某些狀況時,把一小包資料寫進 Cloudflare R2 的某個資料夾。最直覺的作法是把 R2 的存取金鑰直接編進程式——結果這正是最大的坑。繞開它的正解,就是本篇的主角 Cloudflare Workers

以下把 Workers 是什麼、適合什麼場景、怎麼設定、怎麼跟 R2 結合,連同踩過的坑一次整理清楚。

一、Cloudflare Workers 是什麼

Workers 是 Cloudflare 的 邊緣 serverless 運算。跟傳統 serverless(例如 AWS Lambda 用容器)不同,Workers 跑在 V8 isolate 上——就是 Chrome 那顆 JS 引擎的隔離沙箱。這帶來兩個實際差異:

  • 冷啟動幾乎為零:isolate 比容器輕太多,通常 5ms 內就緒,不會有 Lambda 那種第一發卡好幾秒的體感。
  • 跑在離使用者最近的節點:你的程式碼部署到 Cloudflare 全球的邊緣網路,請求打到哪個城市就在那裡執行。

寫的是標準的 JavaScript / TypeScript(也支援 Rust/WASM),進入點是一個 fetch handler:

export default {
  async fetch(request, env, ctx) {
    return new Response("Hello from the edge!");
  },
};
Code language: JavaScript (javascript)

request 是進來的 HTTP 請求,env 是「綁定」(binding,等一下結合 R2 的關鍵),ctx 讓你做 waitUntil() 之類的背景工作。就這麼單純。

用量與費用(撰文時 2026 年,數字會變,以官方為準)

免費方案 付費方案(Workers Paid,約 $5/月起)
請求數 100,000 次/天 1,000 萬次/月起,超出計量
CPU 時間 10 ms / 次請求 可調高(預設 30s 上限)

重點:免費額度對「內部工具、回報端點、小型 API」綽綽有餘。我那個回報端點一天不到幾百次請求,完全落在免費範圍。

二、適用什麼場景

Workers 的強項是「輕、快、貼邊」,所以它特別適合:

  • API / BFF(Backend for Frontend):前端與其自己接一堆第三方服務,不如讓 Worker 當中介層,統一驗證、聚合、改寫。
  • 把金鑰藏在伺服器端的代理:這就是我的案例——client 不該持有的憑證(R2、第三方 API key)放 Worker,client 只跟 Worker 講話。這是 Workers 最被低估、也最實用的用途之一。
  • 邊緣改寫與路由:A/B 測試、地區導流、加 header、擋爬蟲、redirect。
  • Webhook 接收端:GitHub / Stripe / LINE 的 webhook 打進來,Worker 驗簽後轉發或落庫。
  • 靜態網站的動態補丁:搭配 Cloudflare Pages,用 Functions 補上表單、計數器等需要後端的一小塊。

不適合的場景也要誠實講:需要長時間 CPU 運算(影片轉檔、大型 ML 推論)、需要開 TCP 長連線到自家資料庫、或吃大量記憶體的工作,Workers 的 CPU 時間與記憶體上限會咬人,這種請乖乖用容器或 VM。

三、怎麼設定

兩條路:儀表板(點一點)或 wrangler CLI(可版控、可 CI)。正式專案建議用 CLI。

路線 A:儀表板快速起步

  1. Workers & Pages → Create → Create Worker
  2. 取名、Deploy 預設的 hello world,會拿到一個 https://<名字>.<你的子網域>.workers.dev 網址。
  3. Edit code 貼上你的程式碼,再 Deploy。

適合驗證想法、做一次性的小端點。缺點是程式碼不在你的 repo 裡。

路線 B:wrangler CLI(推薦)

# 安裝(或用 npx,不必全域裝)
npm install -g wrangler

# 登入(會開瀏覽器授權)
wrangler login

# 初始化專案
wrangler init my-worker
cd my-worker

# 本地開發(起一個本機伺服器)
wrangler dev

# 部署
wrangler deploy
Code language: PHP (php)

專案根目錄會有一份 wrangler.toml(新版也支援 wrangler.jsonc),這是設定的核心:

name = "my-worker"
main = "src/index.js"
compatibility_date = "2026-01-01"

# 明碼環境變數(會顯示在 dashboard,別放機密)
[vars]
ENVIRONMENT = "production"
Code language: PHP (php)

compatibility_date 很重要:它把 runtime 行為釘在某個日期,避免 Cloudflare 之後改預設行為時把你的 Worker 弄壞。設好就別亂動。

四、怎麼結合 R2

R2 是 Cloudflare 的 S3 相容物件儲存,最大賣點是 egress(下載流量)完全免費(撰文時免費方案含 10GB 儲存、每月 1M 次 Class A 寫入操作、10M 次 Class B 讀取操作)。跟 Worker 結合有兩種存取方式,差別是整篇的重點:

方式一:R2 Binding(強烈推薦)

wrangler.toml 把 bucket 綁進 Worker:

[[r2_buckets]]
binding = "MY_BUCKET"       # 程式裡用 env.MY_BUCKET 存取
bucket_name = "my-bucket"   # 你在 R2 建的 bucket 實際名稱
Code language: PHP (php)

(用儀表板的話:進該 Worker 的 Settings → Bindings → 新增 R2 bucket binding。)

然後在程式裡直接用,完全不需要任何 access key

export default {
  async fetch(request, env) {
    const key = new URL(request.url).pathname.slice(1); // 去掉開頭的 /

    if (request.method === "PUT") {
      await env.MY_BUCKET.put(key, request.body);
      return new Response("OK");
    }

    if (request.method === "GET") {
      const obj = await env.MY_BUCKET.get(key);
      if (!obj) return new Response("Not Found", { status: 404 });
      // 記得帶回 content-type,不然瀏覽器會亂猜
      const headers = new Headers();
      obj.writeHttpMetadata(headers);
      headers.set("etag", obj.httpEtag);
      return new Response(obj.body, { headers });
    }

    return new Response("Method Not Allowed", { status: 405 });
  },
};
Code language: JavaScript (javascript)

env.MY_BUCKET 提供 put / get / delete / list / head。授權由 Cloudflare 內部處理——金鑰從頭到尾不存在於你的程式碼裡。這就是 binding 相對於 S3 API 的殺手級優勢。

方式二:S3 API(金鑰在,慎用)

R2 也給你 S3 相容端點(https://<account_id>.r2.cloudflarestorage.com)與一組 Access Key ID / Secret Access Key,可以用 aws-sdkaws4fetch 存取。只有在「非 Worker 環境」不得不用時才走這條——例如既有的後端服務要接 R2。金鑰務必留在伺服器端。

實戰:一個「只能寫」的上傳端點

把前面的概念收攏成一個真實會用到的樣式——client 只帶一把低權限 token,把 JSON 丟進 classroom/ 前綴,來源 IP 由 Worker 端從連線取得(client 不用自己抓):

export default {
  async fetch(request, env) {
    if (request.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }
    // 共享 token 驗證,擋掉隨便來的人灌資料
    if (request.headers.get("authorization") !== `Bearer ${env.UPLOAD_TOKEN}`) {
      return new Response("Unauthorized", { status: 401 });
    }

    let payload;
    try {
      payload = await request.json();
    } catch {
      return new Response("Bad Request", { status: 400 });
    }

    const record = {
      received_at: new Date().toISOString(),
      source_ip: request.headers.get("cf-connecting-ip") || "unknown",
      ...payload,
    };

    // key 一定要 sanitize,別讓 client 控制路徑(避免覆蓋/穿越)
    const id = String(payload.id || "unknown").replace(/[^a-zA-Z0-9_-]/g, "_");
    const key = `classroom/${id}-${Date.now()}.json`;

    await env.MY_BUCKET.put(key, JSON.stringify(record), {
      httpMetadata: { contentType: "application/json" },
    });

    return new Response(JSON.stringify({ ok: true, key }), {
      status: 200,
      headers: { "content-type": "application/json" },
    });
  },
};
Code language: JavaScript (javascript)

UPLOAD_TOKEN 用 Secret 存(不是明碼 var):

wrangler secret put UPLOAD_TOKEN
# 或儀表板:該 Worker → Settings → Variables and Secrets → 新增,Type 選 Secret
Code language: PHP (php)

這樣一來,發到 client 的只有「Worker 網址 + 一把只能寫的 token」。token 外洩最多讓人往你的端點灌垃圾,rotate 一個 Secret 就止血;R2 bucket 的讀 / 刪權限,client 從頭到尾碰不到。

五、踩坑筆記

這節是這篇的靈魂,都是真的踩過或看別人踩過的。

1. 絕對不要把 R2 的 Secret Access Key 編進前端或桌面 app。 這是最貴的一課。任何發到使用者端的程式(網頁 JS、Electron 的 asar、手機 app)都能被解開。一把有讀寫權的 R2 金鑰外洩,等於整個 bucket 被人下載、竄改、清空。正解就是用 Worker binding 當中介,或簽發短效的 presigned URL。 我一開始差點就直接寫死金鑰,還好懸崖勒馬。

2. 環境變數名稱要跟程式碼 env.XXX 一字不差。 大小寫、拼字錯一個字元,env.UPLOAD_TOKEN 就是 undefined,然後你的驗證整天回 401 卻找不到原因。設完 binding / secret 記得重新 deploy 才生效。

3. 明碼 [vars] 會顯示在儀表板,機密一律用 Secret。 wrangler.toml[vars] 是給非敏感設定用的(環境名、feature flag)。token、金鑰、簽章密鑰請用 wrangler secret put 或儀表板的 Secret 型別,存進去就看不到明碼。

4. 本地 wrangler dev 預設不連真實 R2。 wrangler dev 預設用本地模擬的儲存(方便、不花額度),你在本地 put 進去的東西不會出現在雲端 bucket。要連真實 R2 驗證,加 --remote。第一次沒搞懂會以為「怎麼上傳成功了雲端卻沒東西」。

5. CPU 時間限制是「CPU」不是「等待」。 免費版 10ms 指的是 CPU 運算時間;你 await R2 或 fetch 第三方的等待時間不算 CPU。所以 I/O 密集的代理其實很吃得下免費額度。但要注意 subrequest 數量有上限(Worker 內對外發的請求數,近期還調整過規則),批次轉發很多請求時會撞到。

6. request.body 是 stream,只能讀一次。 如果你想同時「讀 JSON 來驗證」又「原封不動轉發 body」,直接讀兩次會炸。先 await request.json() 存成物件,要轉發時自己重新 JSON.stringify;或用 request.clone() 在讀之前複製一份。

7. 瀏覽器要直接打 Worker,記得處理 CORS。 從網頁 fetch 你的 Worker,跨網域會先來一發 OPTIONS preflight。沒回對 Access-Control-Allow-* header,請求根本到不了你的邏輯。桌面 app / server 對打就沒這問題(我的案例就沒踩到,但網頁前端一定會遇到)。

8. 寫進 R2 的 key 千萬別讓 client 完全控制。 key 直接用使用者傳的字串,等於開放任意覆蓋別人的檔案、甚至用 ../ 之類玩路徑。務必 sanitize(像上面 replace(/[^a-zA-Z0-9_-]/g, "_")),並自己補上時間戳或 UUID 讓 key 唯一。

9. 回傳 R2 物件別忘了 content-type。 env.MY_BUCKET.get() 拿到的 body 你直接 new Response(obj.body) 丟回去,瀏覽器會亂猜型別(圖片變下載、JSON 變純文字)。用 obj.writeHttpMetadata(headers) 把存進去時的 metadata 帶回來。

10. workers.dev 子網域在某些網路會被擋。 免費給的 *.workers.dev 很方便,但部分企業防火牆 / 地區網路會整段封掉這個網域。正式上線請綁自訂網域(在 Worker 的 Triggers/Routes 設定),既穩定又專業。

11. 免費額度以 UTC 每日重置。 每天 10 萬次請求的額度是照 UTC 算的,不是你的當地時區。做用量預估時記得換算,別在跨日高峰被額度打臉。

小結

Cloudflare Workers 對「輕量、貼邊、要藏金鑰的中介層」這類需求是幾乎沒有對手的選擇:冷啟動趨近於零、免費額度大方、跟 R2 一綁就能把儲存金鑰完全留在伺服器端。

如果你只記得一句話,就記這句:凡是會發到使用者端的程式,都不該持有能直接存取儲存後端的金鑰。 用一支 Worker 當守門,client 只拿一把換得掉的低權限 token——這就是我這趟最實在的收穫。

用量與定價數字會隨時間變動,實際請以 [Cloudflare Workers 官方定價文件](https://developers.cloudflare.com/workers/platform/pricing/) 為準。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *