跳轉到

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 會向後端傳遞已驗證的租戶資訊:

X-Tenant-ID: tenant-abc123
X-Tenant-Plan: pro
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

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