跳转到内容
搜索文档

将请求代理到外部 API

最后更新 查看 MarkdownAgent 设置

当沙箱需要调用外部 API 时,你可能会直接将凭据传入沙箱进程。这种方法可行,但意味着沙箱持有一个实时凭据,其中运行的任何代码都可以读取、复制或滥用它。

代理模式消除了该风险。你的 Worker 向沙箱签发短期 JWT 令牌。沙箱将该令牌用于所有 API 请求,这些请求会先到达你的 Worker。Worker 验证 JWT,并在转发请求前注入真实凭据。真实凭据永远不会进入沙箱。

有关涵盖 GitHub、Anthropic 与 R2 的完整多服务实现,请参阅 身份验证示例

工作原理

Sandbox (short-lived JWT) → Worker proxy (validates JWT, injects real credential) → External API

代理框架将请求路由到命名服务。每个服务是一个包含三个字段的 ServiceConfig 对象:

  • target — 要代理到的外部 API 的基础 URL
  • validate — 从传入请求中提取 JWT(返回 null 表示拒绝)
  • transform — 将真实凭据注入转发的请求(或返回 Response 以短路)

你只需定义特定于服务的逻辑。框架处理 JWT 验证、路由与错误响应。

何时使用此模式

在需要以下情况时使用代理模式:

  • 从沙箱调用外部 API 而不暴露凭据
  • 轮换凭据而无需重新配置沙箱
  • 限制沙箱可以执行的操作(例如,限制为特定路径或方法)
  • 在多个沙箱之间共享一个凭据,而无需各自持有副本

对于短期或低风险凭据,环境变量 可能更简单。

前提条件

  • 带有 Sandbox 绑定的 Worker(请参阅 快速入门
  • 在 Worker 项目中安装 jose 包,用于 JWT 签名

1. 设置密钥

在 Worker 中存储 API 凭据以及用于签名 JWT 令牌的密钥:

wrangler secret put MY_API_KEY
wrangler secret put PROXY_JWT_SECRET

PROXY_JWT_SECRET 生成强随机值:

openssl rand -hex 32

2. 安装依赖

npm i jose

3. 复制代理框架

代理框架是一个可复制到项目中的自包含模块。从身份验证示例下载 src/proxy/ 目录,并将其放在 Worker 项目的 src/proxy/

框架导出:

  • createProxyHandler — 创建用于路由与验证代理请求的 Worker 请求处理程序
  • createProxyToken — 为沙箱签发已签名的 JWT
  • ServiceConfig — 你的服务定义所实现的接口

4. 定义服务

为要代理的每个外部 API 创建一个 ServiceConfig。此示例代理一个期望 Bearer 令牌的通用 HTTP API:

export const myApi = {
	// All requests to /proxy/myapi/* are forwarded to this base URL
	target: "https://api.example.com",

	// Extract the JWT from the Authorization header
	validate: (req) =>
		req.headers.get("Authorization")?.replace("Bearer ", "") ?? null,

	// Replace the JWT with the real API key before forwarding
	transform: async (req, ctx) => {
		req.headers.set("Authorization", `Bearer ${ctx.env.MY_API_KEY}`);
		return req;
	},
};
import type { ServiceConfig } from '../proxy';

interface Env {
  MY_API_KEY: string;
  PROXY_JWT_SECRET: string;
}

export const myApi: ServiceConfig<Env> = {
  // All requests to /proxy/myapi/* are forwarded to this base URL
  target: 'https://api.example.com',

  // Extract the JWT from the Authorization header
  validate: (req) =>
    req.headers.get('Authorization')?.replace('Bearer ', '') ?? null,

  // Replace the JWT with the real API key before forwarding
  transform: async (req, ctx) => {
    req.headers.set('Authorization', `Bearer ${ctx.env.MY_API_KEY}`);
    return req;
  }
};

transform 函数接收出站请求以及包含 ctx.env(你的 Worker 环境)与 ctx.jwt(已验证的令牌载荷,包括 sandboxId)的上下文对象。返回修改后的请求以转发它,或返回 Response 以错误短路。

5. 连接 Worker

使用 createProxyHandler 注册服务,并使用 createProxyToken 向沙箱签发令牌:

import { getSandbox } from "@cloudflare/sandbox";
import { createProxyHandler, createProxyToken } from "./proxy";
import { myApi } from "./services/myapi";

export { Sandbox } from "@cloudflare/sandbox";

const proxyHandler = createProxyHandler({
	mountPath: "/proxy",
	jwtSecret: (env) => env.PROXY_JWT_SECRET,
	services: { myapi: myApi },
});

export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		// Route all /proxy/* requests through the proxy handler
		if (url.pathname.startsWith("/proxy/")) {
			return proxyHandler(request, env);
		}

		// Create a sandbox and issue it a short-lived token
		const sandboxId = "my-sandbox";
		const sandbox = getSandbox(env.Sandbox, sandboxId);
		const token = await createProxyToken({
			secret: env.PROXY_JWT_SECRET,
			sandboxId,
			expiresIn: "15m",
		});

		const proxyBase = `https://${url.hostname}`;

		// Pass the token and proxy base URL to the sandbox
		await sandbox.setEnvVars({
			PROXY_TOKEN: token,
			PROXY_BASE: proxyBase,
		});

		return Response.json({ message: "Sandbox ready" });
	},
};
import { getSandbox } from '@cloudflare/sandbox';
import { createProxyHandler, createProxyToken } from './proxy';
import { myApi } from './services/myapi';

export { Sandbox } from '@cloudflare/sandbox';

interface Env {
  Sandbox: DurableObjectNamespace;
  MY_API_KEY: string;
  PROXY_JWT_SECRET: string;
}

const proxyHandler = createProxyHandler<Env>({
  mountPath: '/proxy',
  jwtSecret: (env) => env.PROXY_JWT_SECRET,
  services: { myapi: myApi }
});

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // Route all /proxy/* requests through the proxy handler
    if (url.pathname.startsWith('/proxy/')) {
      return proxyHandler(request, env);
    }

    // Create a sandbox and issue it a short-lived token
    const sandboxId = 'my-sandbox';
    const sandbox = getSandbox(env.Sandbox, sandboxId);
    const token = await createProxyToken({
      secret: env.PROXY_JWT_SECRET,
      sandboxId,
      expiresIn: '15m'
    });

    const proxyBase = `https://${url.hostname}`;

    // Pass the token and proxy base URL to the sandbox
    await sandbox.setEnvVars({
      PROXY_TOKEN: token,
      PROXY_BASE: proxyBase
    });

    return Response.json({ message: 'Sandbox ready' });
  }
};

mountPath/proxy)与服务名称(myapi)共同构成代理路由。对 /proxy/myapi/some/path 的请求会被验证并转发到 https://api.example.com/some/path

6. 从沙箱调用代理

在沙箱内,使用 PROXY_TOKENPROXY_BASE 环境变量调用代理。JWT 代替真实凭据:

curl "$PROXY_BASE/proxy/myapi/v1/endpoint" \
  -H "Authorization: Bearer $PROXY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"input": "hello"}'

或从沙箱内运行的 Python:

import os
import requests

response = requests.post(
    f"{os.environ['PROXY_BASE']}/proxy/myapi/v1/endpoint",
    headers={"Authorization": f"Bearer {os.environ['PROXY_TOKEN']}"},
    json={"input": "hello"}
)

真实的 MY_API_KEY 从不出现在沙箱中。Worker 会透明地替换它。

添加更多服务

要代理其他 API,请定义另一个 ServiceConfig 并将其添加到 createProxyHandler

export const anotherApi = {
	target: "https://api.another-service.com",
	validate: (req) =>
		req.headers.get("Authorization")?.replace("Bearer ", "") ?? null,
	transform: async (req, ctx) => {
		req.headers.set("Authorization", `Bearer ${ctx.env.ANOTHER_API_KEY}`);
		return req;
	},
};

// In your Worker:
const proxyHandler = createProxyHandler({
	mountPath: "/proxy",
	jwtSecret: (env) => env.PROXY_JWT_SECRET,
	services: { myapi: myApi, another: anotherApi },
});
export const anotherApi: ServiceConfig<Env> = {
  target: 'https://api.another-service.com',
  validate: (req) => req.headers.get('Authorization')?.replace('Bearer ', '') ?? null,
  transform: async (req, ctx) => {
    req.headers.set('Authorization', `Bearer ${ctx.env.ANOTHER_API_KEY}`);
    return req;
  }
};

// In your Worker:
const proxyHandler = createProxyHandler<Env>({
  mountPath: '/proxy',
  jwtSecret: (env) => env.PROXY_JWT_SECRET,
  services: { myapi: myApi, another: anotherApi }
});

每个服务可在 /proxy/<service-name>/* 访问。沙箱对所有这些服务使用同一 JWT 令牌。

故障排除

代理返回 401

JWT 缺失、已过期,或使用了错误的密钥签名。请验证:

  • 沙箱使用的是 createProxyToken 返回的令牌,而不是硬编码值
  • 创建与验证令牌使用的是相同的 PROXY_JWT_SECRET
  • 令牌尚未过期——默认值为 15 分钟

要签发新令牌并将其传递给沙箱:

const freshToken = await createProxyToken({
	secret: env.PROXY_JWT_SECRET,
	sandboxId,
	expiresIn: "15m",
});
await sandbox.setEnvVars({ PROXY_TOKEN: freshToken });
const freshToken = await createProxyToken({
  secret: env.PROXY_JWT_SECRET,
  sandboxId,
  expiresIn: '15m'
});
await sandbox.setEnvVars({ PROXY_TOKEN: freshToken });

代理对服务返回 404

URL 中的服务名称必须与 services 对象中的键匹配。对 /proxy/myapi/... 的请求需要 services: { myapi: ... }

transform 返回意外结果

transform 中记录请求 URL,以确认路径被正确重写:

transform: async (req, ctx) => {
	console.log("Proxying to:", req.url);
	req.headers.set("Authorization", `Bearer ${ctx.env.MY_API_KEY}`);
	return req;
};
transform: async (req, ctx) => {
  console.log('Proxying to:', req.url);
  req.headers.set('Authorization', `Bearer ${ctx.env.MY_API_KEY}`);
  return req;
}

相关资源

这篇文档对您有帮助吗?