用 Cloudflare Worker 反代 Supabase,解决国内访问慢

ShuaiQi
#建站#Cloudflare#Supabase

为什么 Supabase 直连慢

Supabase 的托管项目多数部署在海外区域,域名解析出来的 IP 在国内访问往往要经过较长的国际链路。表现是首屏请求慢、登录偶发超时、图片上传卡住,甚至出现 net::ERR_CONNECTION_TIMED_OUT。同一时间段换成手机热点可能就好了,说明不是代码问题,而是网络路径问题。

常见缓解手段有几种:给项目换到更近的区域、在应用层加缓存、把静态资源放到 CDN、或者加一层反向代理。前两种受限于 Supabase 的区域选择和业务逻辑,最后一种改动最小,只要有一个国内访问相对稳定的中转点即可。

Cloudflare 的节点覆盖广,Worker 又提供免费额度,用它把 https://项目.supabase.co 包一层自定义域名,是最省事的做法之一。

反代的原理

Worker 本质上是在 Cloudflare 边缘运行的一段 JavaScript。浏览器请求 https://sb.example.com/rest/v1/posts,Worker 收到后把路径和查询参数原样拼到真实的 Supabase 地址上,发起一次 fetch,再把响应体、状态码和响应头返回给浏览器。

对前端来说,只是把 SUPABASE_URL 从官方域名换成了自己的代理域名,SDK 的其他用法不变。对 Supabase 来说,请求仍然带着原来的 apikey 和 Authorization,鉴权和 RLS 策略照常生效。

需要注意,Worker 不是加速器,它改变的是“浏览器到 Supabase”的链路,实际效果取决于 Cloudflare 节点到 Supabase 区域的连通性。多数情况下会比直连稳定,但不能保证一定快,具体以各地实测为准。

创建 Worker

控制台方式

登录 Cloudflare 控制台,进入 Workers & Pages,创建一个新的 Worker,把默认代码替换成下面的示例,保存并部署。随后在 Settings 里绑定自定义域名,例如 sb.example.com。自定义域名比 *.workers.dev 更稳定,也方便配置缓存和 WAF。

wrangler 方式

本地开发推荐用 CLI,方便版本管理:

bash
npm install -g wrangler
wrangler login
wrangler init supabase-proxy

在 wrangler.toml 里写入上游地址,避免把配置散落在代码中:

toml
name = "supabase-proxy"
main = "src/index.js"
compatibility_date = "2026-11-01"

[vars]
SUPABASE_ORIGIN = "https://你的项目ref.supabase.co"

改完执行 wrangler deploy。变量也可以通过控制台的 Settings -> Variables 配置,两种方式选一种即可。

Worker 代码:转发与 CORS

下面是一份精简实现,包含 OPTIONS 预检、CORS 头、路径与查询参数透传:

js
const ALLOWED_ORIGINS = [
  "https://www.example.com",
  "http://localhost:3000",
];

function buildCorsHeaders(origin) {
  const allowed = ALLOWED_ORIGINS.includes(origin)
    ? origin
    : ALLOWED_ORIGINS[0];

  return {
    "Access-Control-Allow-Origin": allowed,
    "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
    "Access-Control-Allow-Headers":
      "authorization, apikey, content-type, x-client-info, prefer, x-upsert, range",
    "Access-Control-Expose-Headers":
      "content-range, content-profile, x-total-count",
    "Access-Control-Max-Age": "86400",
    "Vary": "Origin",
  };
}

export default {
  async fetch(request, env) {
    const origin = request.headers.get("Origin") || "";
    const cors = buildCorsHeaders(origin);

    if (request.method === "OPTIONS") {
      return new Response(null, { status: 204, headers: cors });
    }

    const incoming = new URL(request.url);
    const target = new URL(
      incoming.pathname + incoming.search,
      env.SUPABASE_ORIGIN
    );

    const headers = new Headers(request.headers);
    headers.delete("host");
    headers.delete("cf-connecting-ip");

    const init = {
      method: request.method,
      headers,
      redirect: "manual",
    };

    if (request.method !== "GET" && request.method !== "HEAD") {
      init.body = request.body;
    }

    const response = await fetch(target.toString(), init);
    const out = new Response(response.body, response);

    for (const [key, value] of Object.entries(cors)) {
      out.headers.set(key, value);
    }

    return out;
  },
};

几个细节值得说明:

  • Access-Control-Allow-Origin 不要无脑写 *。如果前端要携带 Cookie,必须回显具体来源,并额外加上 Access-Control-Allow-Credentials: true。
  • Access-Control-Allow-Headers 要包含 Supabase SDK 实际发送的头,至少是 apikey、authorization、content-type、prefer,上传文件时还会用到 x-upsert。
  • 分页查询依赖 Content-Range 响应头,浏览器默认读不到,需要在 Access-Control-Expose-Headers 里显式暴露。
  • OPTIONS 必须在转发前直接返回,否则预检请求会被送到 Supabase,多一次往返,也容易因为上游不处理而失败。
  • 不要手动设置 Host 头,Cloudflare 不允许;直接删除更安全。cf-connecting-ip 也建议删掉,让上游看到真实来源。

只代理自己的项目

公开可用的代理等于替别人做跳板,既浪费额度,也可能被滥用。至少做三层限制:

第一,上游地址写死在自己的项目上,绝不从查询参数里读取目标地址。像 ?url=https://... 这种设计会让 Worker 变成任意地址转发器。

第二,校验 Origin。上面的代码只允许白名单内的来源拿到正确的 CORS 头,其他来源虽然能拿到响应,但浏览器会拦截。注意这只能防浏览器,防不了 curl。

第三,在 Cloudflare 侧加限速规则或 WAF 规则,对单 IP 的请求频率做限制。如果需要更强的控制,可以校验自定义请求头,但前端代码里的值本质上不是秘密,真正的安全边界仍然是 Supabase 的 RLS 和 anon 权限配置。

还要强调一点:前端只能使用 anon key,service_role key 绝对不能出现在浏览器、移动端或公开仓库里。反代不改变这一点。

前端替换地址

以 @supabase/supabase-js 为例,只改初始化时的 URL。如果前端本身还没搭好,可以先参考 用 Next.js 搭建个人博客并部署到 Cloudflare 完成项目初始化:

ts
import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  "https://sb.example.com",
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  {
    auth: {
      persistSession: true,
      autoRefreshToken: true,
    },
  }
);

如果用 Next.js,把这个值放在环境变量里更规范:

bash
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://sb.example.com
NEXT_PUBLIC_SUPABASE_ANON_KEY=你的 anon key

改完后记得同步 Supabase 控制台的 Auth 配置:Site URL 和 Redirect URLs 仍然填自己的站点域名,不要填代理域名;邮件模板里的链接也保持一致,否则登录回调可能跳到错误地址。

Realtime 与文件上传的注意事项

普通 REST 请求和 Storage 上传可以通过上面的 fetch 正常转发。但 Supabase Realtime 使用的是 WebSocket,Worker 里的普通 fetch 无法完成协议升级,需要单独处理,或者让 Realtime 继续直连官方域名。是否支持以及具体写法以 Cloudflare 与 Supabase 的官方文档为准。

大文件上传会经过 Worker,注意免费套餐的请求体大小和每日请求数限制。如果只是偶尔传图片问题不大,频繁上传大文件建议直传 Storage 或使用支持分片的方式。

常见问题

用了 Worker 就一定快吗?

不一定。它改善的是链路稳定性,实际速度取决于 Cloudflare 节点到 Supabase 区域的质量。建议部署后用真实网络环境对比首屏时间和接口耗时,再决定是否保留。

为什么预检请求返回 204 还是报 CORS 错误?

检查 Access-Control-Allow-Headers 是否覆盖了实际发送的头,以及响应里是否存在重复的 CORS 头。上游如果也返回了 Access-Control-Allow-Origin,要用 set 覆盖而不是追加。

代理域名需要备案吗?

如果使用自己的域名并解析到 Cloudflare,通常涉及域名实名与接入政策,具体以当地监管和 Cloudflare 的要求为准。使用 workers.dev 子域名可以绕开部分配置,但可访问性和稳定性因网络环境而异。

可以把多个 Supabase 项目接到同一个 Worker 吗?

可以,但不建议用查询参数动态选择上游。更稳妥的做法是按路径前缀映射固定项目,或在 Worker 中维护一张显式的映射表,避免变成开放代理。

代理后如何排查 401 或 403?

先在浏览器网络面板确认请求头里的 apikey 和 Authorization 是否完整,再检查 Supabase 的 RLS 策略和表权限。反代只负责转发,不会修改鉴权信息,绝大多数 401 来自 key 配置或登录状态。

评论