Kết xuất ở biên với Cloudflare, kèm dự phòng cục bộ
Tổng quan nhanh
Phần tiêu đề “Tổng quan nhanh”Cầu nối Cloudflare gửi HTML của bạn đến một điểm cuối kết xuất Cloudflare Worker và trả về PDF. Việc kết xuất chạy ở biên, nên bạn không cần vận hành một tiến trình trình duyệt chạy lâu dài. Bạn tạo một cấu hình chỉ dùng HTTPS, kết nối một client PHP Standards Recommendation (PSR)-18 và các factory PSR-17, gọi render(), rồi có thể thêm một bộ kết xuất cục bộ cho các Worker không truy cập được. Hướng dẫn này trình bày lệnh gọi kết xuất, đường dẫn dự phòng, và các biện pháp kiểm soát chống Server-Side Request Forgery (SSRF), chống tấn công đổi địa chỉ Domain Name System (DNS), và ghim khóa công khai Transport Layer Security (TLS) mà cầu nối thực thi trước khi bất kỳ yêu cầu nào rời khỏi tiến trình.
Trước hết, cần chuẩn bị:
- Đã cài đặt NextPDF core và
nextpdf/cloudflare. - Một điểm cuối Worker phục vụ giao kèo kết xuất qua HTTPS và chấp nhận bearer token. Cầu nối từ chối URL Worker không phải HTTPS trước khi gửi bất cứ thứ gì.
- Có sẵn một client PSR-18 (ví dụ Guzzle 7) cùng các factory yêu cầu và luồng PSR-17. Đối với transport cURL có ghim khóa, hãy cung cấp thêm một response factory PSR-17 và
ext-curl. - Đối với dự phòng cục bộ, có sẵn
nextpdf/artisan(hoặc một bộ kết xuất cục bộ khác).
Đây là hướng dẫn thực hành. Để có lần kết xuất chạy được đầu tiên, hãy bắt đầu với phần khởi động nhanh Cloudflare.
Cài đặt
Phần tiêu đề “Cài đặt”Cài đặt cầu nối, một client PSR-18 và các factory PSR-17.
composer require nextpdf/cloudflare guzzlehttp/guzzleĐối với dự phòng cục bộ, hãy cài đặt một bộ kết xuất cục bộ mà cầu nối có thể gọi.
composer require nextpdf/artisanHãy nạp bearer token của Worker và mọi thông tin xác thực R2 từ biến môi trường hoặc trình quản lý bí mật. Đừng bao giờ commit chúng.
Tổng quan khái niệm
Phần tiêu đề “Tổng quan khái niệm”CloudflareHtmlRenderer::render() xác thực HTML và đích đến, gửi một POST đã xác thực đến Worker, rồi phân tích phản hồi. Worker trả về các byte PDF thô (Content-Type: application/pdf) hoặc một thân JSON có trường pdf dạng base64. Bộ kết xuất ánh xạ phản hồi thành một final readonly CloudflareRenderResult chứa các byte, chiều rộng yêu cầu, chiều cao, vị trí kết xuất (suy ra từ header CF-Ray), và thời gian kết xuất.
Cầu nối tách các lỗi thành hai lớp rõ ràng:
CloudflareRenderException— Worker đã trả lời nhưng quá trình kết xuất thất bại (một lỗi HTTP hoặc một thân không bắt đầu bằng%PDF). Đây là lỗi kết xuất và không bao giờ được thử lại bằng dự phòng.CloudflareNotAvailableException— không thể truy cập được biên và không có dự phòng dùng được nào khả dụng.
Dự phòng cục bộ xử lý trường hợp thứ hai. Khi không thể truy cập được Worker và fallbackToLocal là true, cầu nối gọi LocalRendererFactoryInterface mà bạn cung cấp. Việc này diễn ra theo kiểu khởi tạo lười: create() của factory chỉ chạy trên đường dẫn dự phòng. Trong một lần kết xuất dự phòng, renderLocation của kết quả là chuỗi ký tự nguyên văn local.
Cầu nối bảo vệ ranh giới mạng trước khi bất kỳ yêu cầu nào rời khỏi PHP. Nó từ chối URL Worker không phải HTTPS. Nó từ chối một host Worker phân giải về không gian địa chỉ riêng tư hoặc dành riêng, đồng thời kiểm tra tất cả bản ghi A và AAAA, không chỉ bản ghi đầu tiên. Nó cũng phân giải lại host ngay trước khi kết nối, nhờ đó khép lại cửa sổ time-of-check/time-of-use (TOCTOU) trước tấn công đổi địa chỉ DNS. Khi bạn cung cấp một response factory PSR-17 cùng với một tập IP đã phân giải hoặc các pin Subject Public Key Info (SPKI), cầu nối sẽ dùng một transport cURL có ghim khóa. Transport đó ràng buộc kết nối với các IP đã được kiểm duyệt (CURLOPT_RESOLVE), thực thi ghim khóa công khai TLS (CURLOPT_PINNEDPUBLICKEY), xác minh peer và host, và không đi theo chuyển hướng.
Bề mặt API
Phần tiêu đề “Bề mặt API”// Configuration (final readonly):new CloudflareRendererConfig( string $workerUrl, // required, must be HTTPS string $apiToken, // required, #[SensitiveParameter] int $renderTimeout = 30, string $defaultCss = '', int $maxHtmlSize = 5_000_000, ?string $r2FontBucket = null, bool $fallbackToLocal = true, list<string> $pinnedPublicKeys = [], // sha256/<base64> list<string> $backupPublicKeys = [],)CloudflareRendererConfig::fromArray(array $config): self
// The renderer:new CloudflareHtmlRenderer( CloudflareRendererConfig $config, ClientInterface $httpClient, // PSR-18 RequestFactoryInterface $requestFactory, // PSR-17 StreamFactoryInterface $streamFactory, // PSR-17 ?LoggerInterface $logger = null, // PSR-3 ?LocalRendererFactoryInterface $localRendererFactory = null, ?HtmlSecurityPolicyInterface $htmlSecurityPolicy = null, ?ResponseFactoryInterface $responseFactory = null, // enables pinned transport)CloudflareHtmlRenderer::render(string $html, float $widthPt = 595.28, float $heightPt = 0.0, list<string> $fontFiles = []): CloudflareRenderResultCloudflareHtmlRenderer::isAvailable(): boolrender() mặc định dùng chiều rộng A4 (595.28 điểm) và chiều cao tự phát hiện (heightPt: 0). Để xem tham chiếu đầy đủ về các trường và ánh xạ khóa fromArray(), hãy xem trang cấu hình Cloudflare trong phần Xem thêm.
Mẫu mã — Khởi động nhanh
Phần tiêu đề “Mẫu mã — Khởi động nhanh”Tạo cấu hình, dựng bộ kết xuất, kết xuất, và ghi các byte.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;use GuzzleHttp\Psr7\HttpFactory;use NextPDF\Cloudflare\CloudflareHtmlRenderer;use NextPDF\Cloudflare\CloudflareRendererConfig;use NextPDF\Cloudflare\Exception\CloudflareNotAvailableException;use NextPDF\Cloudflare\Exception\CloudflareRenderException;
$config = new CloudflareRendererConfig( workerUrl: 'https://pdf-renderer.example.workers.dev/render', apiToken: getenv('CF_PDF_TOKEN') ?: throw new RuntimeException('CF_PDF_TOKEN not set'),);
$httpFactory = new HttpFactory();
$renderer = new CloudflareHtmlRenderer( config: $config, httpClient: new Client(), requestFactory: $httpFactory, streamFactory: $httpFactory, responseFactory: $httpFactory, // enables the pinned cURL transport);
try { $result = $renderer->render('<h1>Hello from the edge</h1>');
if (!$result->isValid()) { throw new RuntimeException('Worker did not return a valid PDF'); }
file_put_contents('output.pdf', $result->pdfData);} catch (CloudflareRenderException $exception) { // Worker answered but the render failed. Not retried with fallback. fwrite(STDERR, 'Render failed: ' . $exception->getMessage() . PHP_EOL); exit(1);} catch (CloudflareNotAvailableException $exception) { // Edge unreachable and no usable fallback. fwrite(STDERR, 'Edge unavailable: ' . $exception->getMessage() . PHP_EOL); exit(2);}Token lấy từ môi trường và không bao giờ được mã hóa cứng. workerUrl phải dùng HTTPS; cầu nối từ chối một URL http:// trước khi gửi bất kỳ yêu cầu nào.
Mẫu mã — Production
Phần tiêu đề “Mẫu mã — Production”Trong môi trường production, hãy kết nối một local renderer factory để khi Worker không truy cập được, yêu cầu sẽ chuyển sang dự phòng thay vì thất bại. Hãy cấu hình các pin TLS kèm một pin dự phòng. create() của factory chỉ chạy trên đường dẫn dự phòng.
<?php
declare(strict_types=1);
use NextPDF\Artisan\ChromeHtmlRenderer;use NextPDF\Cloudflare\Contract\LocalRendererFactoryInterface;use NextPDF\Cloudflare\Contract\LocalRendererInterface;
final readonly class ArtisanLocalRendererFactory implements LocalRendererFactoryInterface{ public function __construct(private ChromeHtmlRenderer $chrome) {}
public function create(): LocalRendererInterface { return new readonly class($this->chrome) implements LocalRendererInterface { public function __construct(private ChromeHtmlRenderer $chrome) {}
/** @param array<string, mixed> $options */ public function render(string $html, array $options = []): string { $widthPt = (float) ($options['widthPt'] ?? 595.28); // A4 width $heightPt = (float) ($options['heightPt'] ?? 0.0); // 0 = auto-fit
return $this->chrome->render($html, $widthPt, $heightPt)->getPdfData(); } }; }}Kết nối factory và các pin vào bộ kết xuất.
<?php
declare(strict_types=1);
use NextPDF\Cloudflare\CloudflareHtmlRenderer;use NextPDF\Cloudflare\CloudflareRendererConfig;
$config = CloudflareRendererConfig::fromArray([ 'worker_url' => getenv('CF_WORKER_URL') ?: '', 'api_token' => getenv('CF_PDF_TOKEN') ?: '', 'render_timeout' => 60, 'fallback_to_local' => true, 'pinned_public_keys' => ['sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg='], 'backup_public_keys' => ['sha256/Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys='],]);
$renderer = new CloudflareHtmlRenderer( config: $config, httpClient: $httpClient, requestFactory: $httpFactory, streamFactory: $httpFactory, logger: $logger, localRendererFactory: new ArtisanLocalRendererFactory($chrome), responseFactory: $httpFactory,);Khi dự phòng chạy, renderLocation của kết quả là local và heightPt là 0.0. Cầu nối ghi log dự phòng ở mức warning, rồi info. Hãy luôn cấu hình một pin dự phòng trước khi xoay vòng chứng chỉ, để một lần xoay vòng theo kế hoạch không khóa cầu nối khỏi điểm cuối.
Trường hợp đặc biệt & điểm cần lưu ý
Phần tiêu đề “Trường hợp đặc biệt & điểm cần lưu ý”- Lỗi Worker không phải là lỗi không truy cập được. Một Worker trả về lỗi HTTP hoặc thân không đúng định dạng sẽ phát sinh
CloudflareRenderExceptionvà không bao giờ được thử lại bằng dự phòng. Chỉ khi biên không truy cập được thì mới chuyển sang dự phòng. Hãy giữ hai nhánh catch tách biệt. - Dự phòng cần cả cờ lẫn một factory. Với
fallbackToLocal: truenhưng không có factory nào được kết nối, một Worker không truy cập được sẽ phát sinhCloudflareNotAvailableExceptionvà nêu tên factory bị thiếu. Hãy kết nối factory. isAvailable()là một gợi ý, không phải một bảo đảm. Nó gửi mộtHEADđã xác thực và trả vềtruecho một trạng thái dưới500; lệnhPOSTkế tiếp vẫn có thể thất bại. Đừng coi nó là một giao kèo.- Ghim khóa là tùy chọn bật. Một tập pin rỗng sẽ tắt ghim khóa. Chỉ dùng tập rỗng với một chuỗi chứng chỉ ổn định, đã biết, và hãy giữ một pin dự phòng một khi bạn đã ghim khóa.
fontFilescần một bucket R2. Đối sốfontFileschỉ có ý nghĩa khi cấu hình đặtr2FontBucket; nếu không, nó không có tác dụng.- Cầu nối không ký. Nó trả về các byte PDF. Hãy kết xuất ở biên, rồi ký trong tiến trình của riêng bạn, để khóa ký không bao giờ vượt qua ranh giới biên.
Hiệu năng
Phần tiêu đề “Hiệu năng”Kết xuất ở biên chuyển chi phí trình duyệt ra khỏi các host của bạn. Bạn vẫn phải trả cho một vòng đi-về HTTPS đến Worker cộng với thời gian kết xuất của Worker, được kết quả báo cáo dưới dạng renderTimeMs. Cầu nối áp dụng thời gian chờ đã cấu hình thông qua transport có ghim khóa. Hãy đặt thời gian chờ dựa trên độ trễ Worker đã đo được, kèm dư địa, và giữ nó dưới mọi thời gian chờ của gateway thượng nguồn. Gói chỉ nêu các giới hạn mà chính nó thực thi. Nó không đưa ra tuyên bố nào về trần CPU, bộ nhớ hay thân yêu cầu của nền tảng Cloudflare. Đối với các giới hạn đó, hãy tham khảo tài liệu của Cloudflare và Worker của bạn.
Ghi chú bảo mật
Phần tiêu đề “Ghi chú bảo mật”- Đích đến được xác thực trước khi yêu cầu rời khỏi PHP. Các URL không phải HTTPS bị từ chối. Một host phân giải về không gian địa chỉ riêng tư hoặc dành riêng sẽ bị từ chối trên tất cả bản ghi A và AAAA. Host được phân giải lại ngay trước khi kết nối để phòng thủ chống tấn công đổi địa chỉ DNS.
- Transport có ghim khóa ràng buộc DNS và TLS. Với một response factory và các pin đã cấu hình, cầu nối ràng buộc kết nối với các IP đã được kiểm duyệt, thực thi ghim khóa SPKI, xác minh peer và host, và từ chối đi theo chuyển hướng đến một host chưa được kiểm duyệt.
- Đầu vào được giới hạn. HTML vượt quá
maxHtmlSize(mặc định 5 MB), data URI base64 quá khổ, và mọi thẻ<meta http-equiv="refresh">đều bị từ chối trước khi yêu cầu được gửi. - Bí mật được che giấu và bất biến.
apiTokenvà các khóa R2 mang#[SensitiveParameter], nên stack trace sẽ che giấu chúng, và các đối tượng cấu hình làfinal readonly. Hãy nạp bí mật từ môi trường hoặc một trình quản lý bí mật; đừng bao giờ commit chúng. - Đừng bao giờ viết một khối
catchrỗng. Mỗi ví dụ bắt đúng loại ngoại lệ cụ thể và ghi log hoặc thoát với một mã đã định.
Mô hình bảo mật đầy đủ nằm trên trang security-and-operations của Cloudflare trong phần Xem thêm. Trang đó bao quát phòng thủ SSRF và chống tấn công đổi địa chỉ DNS, thao tác ghim khóa, xử lý bí mật, và các điều khoản OWASP và RFC 7469 liên quan.
Tuân thủ
Phần tiêu đề “Tuân thủ”Hướng dẫn này không tự đưa ra tuyên bố tiêu chuẩn quy phạm nào. Trên các trang security-and-operations và cấu hình thượng nguồn của Cloudflare, việc phân giải DNS toàn bộ bản ghi và kiểm tra lại TOCTOU của cầu nối ánh xạ tới hướng dẫn phòng chống SSRF của OWASP, còn việc ghim khóa công khai TLS và phục hồi bằng pin dự phòng của nó ánh xạ tới RFC 7469. Trang cookbook này chỉ trình bày lại cách sử dụng và để các trích dẫn đó cho các trang kia. Cầu nối không thực hiện việc ký và không đưa ra tuyên bố tuân thủ về chữ ký.
Xem thêm
Phần tiêu đề “Xem thêm”- Kết xuất HTML thành PDF bằng bộ kết xuất Artisan Chrome — bộ kết xuất trong tiến trình được dùng làm dự phòng cục bộ tại đây.
- Khởi động nhanh Cloudflare — lần kết xuất ở biên đầu tiên và mô hình kết quả.
- Bảo mật và vận hành Cloudflare — SSRF, chống tấn công đổi địa chỉ DNS, ghim khóa, và xoay vòng bí mật.
- Sử dụng Cloudflare trong production — kết nối dự phòng, đo lường từ xa, lưu trữ R2, và bảo vệ API.