当沙箱需要调用外部 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 的基础 URLvalidate— 从传入请求中提取 JWT(返回null表示拒绝)transform— 将真实凭据注入转发的请求(或返回Response以短路)
你只需定义特定于服务的逻辑。框架处理 JWT 验证、路由与错误响应。
在需要以下情况时使用代理模式:
- 从沙箱调用外部 API 而不暴露凭据
- 轮换凭据而无需重新配置沙箱
- 限制沙箱可以执行的操作(例如,限制为特定路径或方法)
- 在多个沙箱之间共享一个凭据,而无需各自持有副本
对于短期或低风险凭据,环境变量 可能更简单。
在 Worker 中存储 API 凭据以及用于签名 JWT 令牌的密钥:
wrangler secret put MY_API_KEY
wrangler secret put PROXY_JWT_SECRET为 PROXY_JWT_SECRET 生成强随机值:
openssl rand -hex 32npm i joseyarn add josepnpm add josebun add jose代理框架是一个可复制到项目中的自包含模块。从身份验证示例下载 src/proxy/ ↗ 目录,并将其放在 Worker 项目的 src/proxy/。
框架导出:
createProxyHandler— 创建用于路由与验证代理请求的 Worker 请求处理程序createProxyToken— 为沙箱签发已签名的 JWTServiceConfig— 你的服务定义所实现的接口
为要代理的每个外部 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 以错误短路。
使用 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。
在沙箱内,使用 PROXY_TOKEN 与 PROXY_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 令牌。
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 });URL 中的服务名称必须与 services 对象中的键匹配。对 /proxy/myapi/... 的请求需要 services: { myapi: ... }。
在 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;
}