API Gateway(Cloudflare Workers)¶
NextPDF nextpdf/cloudflare 套件提供 ApiProtection 元件,將 Cloudflare Workers 作為邊緣 API Gateway,在距離使用者最近的節點執行認證、速率限制與冪等性檢查。
為何選擇 Cloudflare Workers?¶
| 特性 | 傳統 API Gateway | Cloudflare Workers |
|---|---|---|
| 全球延遲 | 50-200ms(集中式) | < 10ms(330+ PoP) |
| 擴展能力 | 手動配置 | 自動全球擴展 |
| DDoS 防護 | 需額外設定 | 內建 Anycast |
| 冷啟動 | 秒級(容器) | 0ms(V8 Isolate) |
| 成本 | 固定費用 | 按請求計費 |
ApiProtection 架構¶
flowchart LR
CLIENT["API 客戶端\n(租戶應用)"]
subgraph CF["Cloudflare Workers(全球邊緣)"]
direction TB
AUTH["AuthMiddleware\nBearer / JWT / API Key 驗證"]
TENANT["TenantResolver\n解析 tenant_id 與 plan"]
RL["RateLimiter\n令牌桶(每租戶獨立)"]
IDEM["IdempotencyGuard\nKV 去重(防重複請求)"]
CACHE["EdgeCache\nCloudflare Cache API"]
PROXY["OriginProxy\n轉發至 PHP 後端"]
end
ORIGIN["PHP 應用後端\n(K8s / VPS)"]
CLIENT --> AUTH --> TENANT --> RL --> IDEM --> CACHE --> PROXY --> ORIGIN 安裝與設定¶
Wrangler 設定¶
# wrangler.toml
name = "nextpdf-api-gateway"
main = "src/index.ts"
compatibility_date = "2026-03-01"
compatibility_flags = ["nodejs_compat"]
[[kv_namespaces]]
binding = "IDEMPOTENCY_STORE"
id = "your-kv-namespace-id"
[[kv_namespaces]]
binding = "RATE_LIMIT_STORE"
id = "your-rate-limit-kv-id"
[vars]
NEXTPDF_ORIGIN = "https://api.your-backend.com"
JWT_AUDIENCE = "nextpdf-api"
LOG_LEVEL = "warn"
[[secrets]]
name = "JWT_SECRET"
Workers 入口點¶
// src/index.ts
import { ApiProtection } from '@nextpdf/cloudflare';
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const protection = new ApiProtection({
auth: {
mode: 'jwt',
secret: env.JWT_SECRET,
audience: env.JWT_AUDIENCE,
issuerAllowlist: ['your-saas-platform'],
},
rateLimit: {
store: env.RATE_LIMIT_STORE,
defaultLimits: {
requestsPerMinute: 60,
requestsPerDay: 10000,
},
tenantOverrides: {
plan_enterprise: { requestsPerMinute: 1000, requestsPerDay: 500000 },
plan_pro: { requestsPerMinute: 300, requestsPerDay: 50000 },
},
},
idempotency: {
store: env.IDEMPOTENCY_STORE,
ttlSeconds: 86400,
headerName: 'Idempotency-Key',
},
origin: {
url: env.NEXTPDF_ORIGIN,
timeoutMs: 30000,
retries: 1,
},
});
return protection.handle(request, ctx);
},
};
認證機制¶
Bearer Token(API Key)¶
適合伺服器對伺服器(S2S)的簡單整合:
GET /v1/generate HTTP/1.1
Host: api.yourpdf.com
Authorization: Bearer npro_sk_live_abc123...
Content-Type: application/json
ApiProtection 會向後端傳遞已驗證的租戶資訊:
JWT(Bearer JWT)¶
適合需要細粒度 Claims 驗證的場景:
// JWT Claims 驗證設定
auth: {
mode: 'jwt',
algorithm: 'HS256', // 或 'RS256' / 'ES256'
secret: env.JWT_SECRET,
requiredClaims: ['tenant_id', 'plan', 'permissions'],
clockTolerance: 30, // 允許 30 秒時鐘偏差
}
速率限制¶
令牌桶演算法¶
RateLimiter 採用令牌桶(Token Bucket)演算法,支援: - 每租戶獨立限制:根據 tenant_id 分配獨立的令牌桶 - 多時間視窗:同時執行分鐘、小時、日三種粒度的限制 - 計劃升級:plan_enterprise 自動獲得更高限制 - 動態調整:可透過管理 API 即時調整特定租戶的限制
// 速率限制回應標頭
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1740000060
// 超過限制時
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/problem+json
{
"type": "https://docs.yourpdf.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "You have exceeded 60 requests per minute. Retry after 30 seconds.",
"retry_after": 30
}
冪等性保護¶
冪等性機制確保因網路問題重複發送的請求只被處理一次,防止重複計費。
請求方式¶
POST /v1/generate HTTP/1.1
Idempotency-Key: unique-client-generated-key-123
Content-Type: application/json
{"html": "<h1>Report</h1>"}
工作原理¶
sequenceDiagram
participant Client
participant KV as Cloudflare KV
participant Origin as PHP 後端
Client->>KV: 查詢 idempotency_key 是否存在
alt Key 不存在(新請求)
KV-->>Client: 不存在
Client->>Origin: 轉發請求
Origin-->>Client: 200 OK + 回應體
Client->>KV: 儲存 key → 回應體(TTL: 24h)
else Key 已存在(重複請求)
KV-->>Client: 命中快取
Client-->>Client: 直接返回快取回應(不轉發至後端)
end 客戶端最佳實踐¶
// 客戶端生成冪等性 Key(建議基於業務邏輯生成,而非隨機)
$idempotencyKey = hash('sha256', "tenant-{$tenantId}:operation:generate:{$documentId}:v1");
$response = $httpClient->post('/v1/generate', [
'headers' => [
'Authorization' => "Bearer {$apiKey}",
'Idempotency-Key' => $idempotencyKey,
],
'json' => ['html' => $htmlContent],
]);
邊緣緩存¶
可緩存端點¶
| 端點 | 緩存 TTL | Cache Key | 說明 |
|---|---|---|---|
GET /v1/documents/{id} | 300s | tenant_id + document_id | 文件元資料 |
GET /v1/fonts | 3600s | 靜態 | 可用字型清單 |
GET /health | 30s | 靜態 | 健康檢查 |
POST /v1/generate | 不緩存 | — | 寫入操作 |
POST /v1/parse | 條件緩存 | tenant_id + file_hash | 相同文件的解析結果 |
緩存控制設定¶
// 設定端點緩存策略
caching: {
rules: [
{
pattern: 'GET /v1/documents/*',
ttl: 300,
cacheKey: (req, ctx) => `${ctx.tenantId}:${req.url}`,
vary: ['Accept-Language'],
},
{
pattern: 'POST /v1/parse',
ttl: 3600,
cacheKey: async (req, ctx) => {
const body = await req.json();
return `${ctx.tenantId}:parse:${body.file_hash}`;
},
condition: (req) => req.headers.get('X-Allow-Cache') === 'true',
},
],
}
請求/回應日誌¶
ApiProtection 自動記錄所有請求至 Cloudflare Logpush,格式為結構化 JSON:
{
"timestamp": "2026-03-04T10:30:00Z",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "tenant-abc123",
"plan": "pro",
"method": "POST",
"path": "/v1/generate",
"status": 200,
"duration_ms": 1234,
"rate_limit_remaining": 45,
"idempotency_cache_hit": false,
"edge_location": "TPE",
"user_agent": "NextPDF-PHP-Client/1.0"
}
部署¶
# 本地開發
wrangler dev
# 部署至 Cloudflare
wrangler deploy --env production
# 設定 JWT 密鑰
wrangler secret put JWT_SECRET --env production
參見¶
Commercial License
This feature requires a commercial license. Contact our team for pricing and deployment support.
Contact Sales