跳轉到

API 保護

ApiProtection 子系統為 Cloudflare Workers 上的 PDF 生成端點提供雙層防護:API 金鑰驗證確保只有授權客戶端可存取,KV-based 速率限制防止端點被過度呼叫或濫用。


PHP Compatibility

This example uses PHP 8.5 syntax. If your environment runs PHP 8.1 or 7.4, use NextPDF Backport for a backward-compatible build.

API 金鑰驗證

設定 API 金鑰驗證器

<?php

declare(strict_types=1);

use NextPDF\Cloudflare\ApiProtection\ApiKeyValidator;
use NextPDF\Cloudflare\ApiProtection\Config\ApiKeyConfig;

$validator = ApiKeyValidator::create(
    config: ApiKeyConfig::create(
        headerName: 'X-NextPDF-API-Key',       // 客戶端傳遞金鑰的 Header 名稱
        kvNamespace: $env['KV_API_KEYS'],       // Cloudflare KV Namespace binding
        keyPrefix: 'api_key:',                  // KV 中的鍵名前綴
        hashAlgorithm: 'sha256',                // 儲存雜湊值,非明文
        cacheSeconds: 60,                       // 本地快取驗證結果(秒)
    ),
);

在 Worker Handler 中驗證

function handle(array $request, array $env): array
{
    $validator = ApiKeyValidator::create(/* ... */);

    // 驗證 API 金鑰;失敗時拋出 ApiKeyInvalidException
    $apiKeyContext = $validator->validate($request['headers']['x-nextpdf-api-key'] ?? '');

    // $apiKeyContext 包含客戶端識別資訊(用於速率限制)
    $clientId = $apiKeyContext->clientId(); // non-empty-string

    // ... 繼續 PDF 生成
}

API 金鑰管理

API 金鑰以 SHA-256 雜湊值儲存於 Cloudflare KV,使用 Wrangler CLI 管理:

# 生成新 API 金鑰
openssl rand -hex 32

# 計算雜湊值並存入 KV
echo -n "your-api-key-here" | sha256sum
wrangler kv:key put --binding=KV_API_KEYS \
    "api_key:a1b2c3d4..." \
    '{"clientId":"customer-42","plan":"pro","enabled":true}'

# 停用金鑰
wrangler kv:key put --binding=KV_API_KEYS \
    "api_key:a1b2c3d4..." \
    '{"clientId":"customer-42","plan":"pro","enabled":false}'

KV 中儲存的金鑰元資料格式:

{
    "clientId": "customer-42",
    "plan": "pro",
    "enabled": true,
    "rateLimit": {
        "requestsPerMinute": 60,
        "requestsPerDay": 1000
    },
    "createdAt": "2026-03-04T00:00:00Z"
}

速率限制

設定速率限制器

use NextPDF\Cloudflare\ApiProtection\RateLimiter;
use NextPDF\Cloudflare\ApiProtection\Config\RateLimitConfig;

$rateLimiter = RateLimiter::create(
    config: RateLimitConfig::create(
        kvNamespace: $env['KV_RATE_LIMITS'],    // 速率計數器 KV Namespace
        windowSeconds: 60,                       // 滑動視窗大小(秒)
        maxRequests: 60,                         // 視窗內最大請求數
        keyPrefix: 'rate:',                      // KV 計數器鍵前綴
        algorithm: 'sliding_window',             // 演算法:sliding_window 或 fixed_window
    ),
);

執行速率限制

use NextPDF\Cloudflare\ApiProtection\RateLimitResult;

$result = $rateLimiter->check(
    clientId: $apiKeyContext->clientId(),
    cost: 1, // 此請求消耗的配額單位(大型 PDF 可設為較高值)
);

if ($result->exceeded()) {
    return [
        'status'  => 429,
        'headers' => [
            'Content-Type'          => 'application/json',
            'Retry-After'           => (string) $result->retryAfterSeconds(),
            'X-RateLimit-Limit'     => (string) $result->limit(),
            'X-RateLimit-Remaining' => '0',
            'X-RateLimit-Reset'     => (string) $result->resetAt()->getTimestamp(),
        ],
        'body'    => json_encode(['error' => 'rate_limit_exceeded', 'retryAfter' => $result->retryAfterSeconds()]),
    ];
}

組合使用(完整保護流程)

<?php

declare(strict_types=1);

use NextPDF\Cloudflare\ApiProtection\ApiKeyValidator;
use NextPDF\Cloudflare\ApiProtection\RateLimiter;
use NextPDF\Cloudflare\EdgeRenderer;
use NextPDF\Cloudflare\Http\WorkerResponse;
use NextPDF\Cloudflare\ApiProtection\Exception\ApiKeyInvalidException;
use NextPDF\Cloudflare\ApiProtection\Exception\RateLimitExceededException;

function handle(array $request, array $env): array
{
    $validator   = ApiKeyValidator::create(/* config */);
    $rateLimiter = RateLimiter::create(/* config */);
    $renderer    = EdgeRenderer::create(/* config */);

    try {
        // 第一層:驗證 API 金鑰
        $ctx = $validator->validate($request['headers']['x-nextpdf-api-key'] ?? '');

        // 第二層:速率限制
        $rateLimiter->enforce(clientId: $ctx->clientId(), cost: 1);

        // 生成 PDF
        $pdfBytes = $renderer->render(function (\NextPDF\Core\Document $doc): void {
            $doc->addPage()->text('Protected PDF', x: 20, y: 30);
        });

        return WorkerResponse::pdf(bytes: $pdfBytes, filename: 'document.pdf');

    } catch (ApiKeyInvalidException) {
        return WorkerResponse::error(status: 401, message: 'invalid_api_key');
    } catch (RateLimitExceededException $e) {
        return WorkerResponse::error(status: 429, message: 'rate_limit_exceeded', headers: [
            'Retry-After' => (string) $e->retryAfterSeconds(),
        ]);
    }
}

CORS 設定

對於需要從瀏覽器直接呼叫的端點:

use NextPDF\Cloudflare\ApiProtection\CorsConfig;
use NextPDF\Cloudflare\Http\WorkerResponse;

$cors = CorsConfig::create(
    allowedOrigins: ['https://app.example.com'],
    allowedMethods: ['POST'],
    allowedHeaders: ['X-NextPDF-API-Key', 'Content-Type'],
    maxAge: 86400,
);

// 處理 OPTIONS preflight 請求
if ($request['method'] === 'OPTIONS') {
    return WorkerResponse::cors($cors);
}

參見