Ein generiertes PDF über eine signierte, ablaufende URL ausliefern
Auf einen Blick
Abschnitt betitelt „Auf einen Blick“Sie erzeugen eine Portable-Document-Format-(PDF-)Datei und müssen sie einem Client aushändigen. Der einfachste Weg streamt die Bytes direkt durch einen Controller, aber das bindet einen Anwendungs-Worker für den gesamten Download, leitet den Traffic durch Ihre Server und exponiert die Datei für jeden, der die Route erreichen kann. Das Auslieferungsmuster auf dieser Seite tut das Gegenteil: erzeugen Sie das PDF, speichern Sie die Bytes in Objektspeicher und geben Sie einen kurzlebigen, signierten Uniform Resource Locator (URL) zurück, den der Client direkt aus dem Speicher abruft. Ihre App gibt eine kleine JavaScript-Object-Notation-(JSON-)Payload mit einer URL zurück; der Speicher liefert die Bytes.
Die NextPDF-Seite ist ein Aufruf: getPdfData() auf dem Dokument gibt das rohe
PDF-Binary als Zeichenkette zurück. Alles danach — das Objekt ablegen und einen
zeitlich begrenzten signierten Link prägen — ist die Aufgabe Ihres Frameworks oder
Ihres Cloud-Anbieters. Die Signatur-Primitive sind echte, dokumentierte APIs:
Laravel Storage::temporaryUrl() und URL::temporarySignedRoute(), Symfony
UriSigner sowie die Presigned-URL-Operationen von Amazon Simple Storage Service
(S3) oder Google Cloud Storage (GCS) in deren Software Development Kits (SDKs).
NextPDF definiert keinen eigenen URL-Helfer; suchen Sie nicht danach.
Prüfen Sie zuerst diese Teile:
- NextPDF Core ist installiert und Sie können ein Dokument bauen.
- Sie verfügen über Objektspeicher, für den das Framework signieren kann: einen S3- oder S3-kompatiblen Bucket, einen GCS-Bucket oder eine Laravel-Disk, deren Treiber Temporary-URLs unterstützt.
- Anmeldedaten liegen in Umgebungsvariablen oder einem Secret-Manager, niemals in eingecheckter Konfiguration.
Dies ist eine betriebliche Anleitung. Sie setzt voraus, dass Sie bereits wissen, wie man eine Anfrage an einen Controller routet. Für die direkte Rückgabe von Bytes stattdessen siehe Ein generiertes PDF aus einem Controller zurückgeben.
Konzeptioneller Überblick
Abschnitt betitelt „Konzeptioneller Überblick“Das Muster hat drei Schritte, und nur der erste berührt NextPDF:
- Erzeugen. Bauen Sie das Dokument und rufen Sie
getPdfData()auf, um die Bytes zu erhalten. - Speichern. Schreiben Sie diese Bytes auf einen Objektspeicher-Key
(
reports/2026/r-42.pdf). - Signieren. Bitten Sie das Framework oder das Cloud-SDK um eine signierte URL zu diesem Key, mit einem Ablauf, und geben Sie die URL an den Client zurück.
Warum speichern und signieren statt die Bytes zu proxyen:
- Bandbreite auslagern. Der Objektspeicher (oder sein Content-Delivery-Network-Edge) liefert den Download. Ihr Anwendungs-Worker gibt ein paar hundert Bytes JSON zurück und ist sofort frei, statt für die Dauer einer mehrere Megabyte großen Übertragung gehalten zu werden.
- Zugriff eingrenzen. Eine signierte URL gewährt Zugriff auf ein Objekt für ein begrenztes Zeitfenster. Der Bucket selbst bleibt privat. Es gibt keine öffentliche Route zum Brute-Forcen und keine breite Bucket-Lese-Berechtigung.
- Ablauf. Die Signatur bettet einen Ablaufzeitstempel ein. Nachdem dieser überschritten ist, ist der Link tot. Eine durchgesickerte URL hört von selbst auf zu funktionieren, was den Wirkungsradius einer versehentlichen Weitergabe begrenzt.
Es gibt zwei verschiedene Signaturmodelle, und sie unterscheiden sich darin, was signiert wird:
- Objektspeicher-Presigned-URLs (S3, GCS oder Laravels
temporaryUrl()über eine S3-/GCS-Disk) zeigen direkt auf das Speicherobjekt. Der Download erreicht Ihre App überhaupt nicht. - Signierte Anwendungsrouten (Laravel
URL::temporarySignedRoute(), SymfonyUriSigner) zeigen auf Ihre eigene Route. Die Anfrage trifft weiterhin Ihre App, die die Signatur verifiziert und dann zum Objekt streamt oder weiterleitet. Verwenden Sie diese, wenn Sie bei jedem Download Autorisierung, Logging oder Abrechnung ausführen müssen oder wenn Ihr Speicher nicht presignen kann.
API-Oberfläche
Abschnitt betitelt „API-Oberfläche“| Belang | NextPDF | Laravel | Symfony |
|---|---|---|---|
| PDF-Bytes holen | NextPDF\Core\Document::getPdfData(): string | same | same |
| Bytes speichern | — | Storage::disk($d)->put($key, $bytes) | Filesystem::dumpFile($path, $bytes) or Flysystem write() |
| Presigned-Storage-URL | — | Storage::disk($d)->temporaryUrl($key, $expiresAt) | AWS/GCS SDK presigner (below) |
| Signierte App-Route | — | URL::temporarySignedRoute($name, $expiresAt, $params) | UriSigner::sign($url) |
| Eine signierte App-Route verifizieren | — | signed route middleware / $request->hasValidSignature() | UriSigner::check() / checkRequest() |
Der einzige NextPDF-Engine-AUFRUF, den dieses Auslieferungsmuster benötigt, ist
getPdfData(); das Dokument selbst wird so gebaut, wie Ihre App Dokumente bereits
baut (z. B. die injizierte DocumentFactoryInterface / die Symfony PdfFactory).
getPdfData() ist im HasOutput-Trait auf NextPDF\Core\Document deklariert. Es
ruft den Writer einmal auf und gibt das gesamte PDF als Zeichenkette zurück. Sein
Pendant save(string $path): void schreibt dieselben Bytes über einen atomaren
Writer auf die Festplatte; verwenden Sie es nur, wenn Ihr Speicher ein echter
lokaler Dateisystempfad ist. Für Objektspeicher bevorzugen Sie getPdfData() und
lassen das Storage-SDK die Übertragung besitzen.
Das Dokument wird gebaut, wenn Sie
getPdfData()(odersave()) aufrufen, und der Build ist nicht idempotent. Rufen Sie es einmal pro Dokument auf, fangen Sie die Zeichenkette ein und verwenden Sie diese Zeichenkette für den Upload und jede Größe oder Prüfsumme wieder, die Sie berechnen.
Codebeispiel — Laravel-Temporary-URL
Abschnitt betitelt „Codebeispiel — Laravel-Temporary-URL“Laravels Dateisystem-Abstraktion signiert für Sie. Auf einer S3- (oder
S3-kompatiblen) Disk gibt Storage::temporaryUrl() eine Presigned-URL direkt zum
Objekt zurück. Der Client lädt aus dem Speicher herunter; Ihre Action gibt nur JSON
zurück.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;use Illuminate\Support\Facades\Storage;use NextPDF\Contracts\DocumentFactoryInterface;use Psr\Log\LoggerInterface;use Throwable;
final class ReportDeliveryController extends Controller{ public function __construct( private readonly DocumentFactoryInterface $documents, private readonly LoggerInterface $logger, ) {}
public function store(int $reportId): JsonResponse { try { // 1. Generate. Build once; getPdfData() returns the raw bytes. $document = $this->documents->create(); $document->addPage(); $document->cell(0, 10, "Report #{$reportId}", newLine: true); $bytes = $document->getPdfData();
// 2. Store under a non-guessable key on a private disk. $key = sprintf('reports/%d/%s.pdf', $reportId, bin2hex(random_bytes(16))); Storage::disk('s3')->put($key, $bytes, ['visibility' => 'private']);
// 3. Sign. A presigned URL straight to the object, valid 10 minutes. $url = Storage::disk('s3')->temporaryUrl($key, now()->addMinutes(10));
return new JsonResponse(['download_url' => $url], 201); } catch (Throwable $exception) { // Log the class, never the message or trace, so detail does not leak. $this->logger->error('Report PDF delivery failed', [ 'report_id' => $reportId, 'exception' => $exception::class, ]);
return new JsonResponse(['error' => 'Could not prepare the report.'], 500); } }}Die Disk muss eine sein, deren Treiber Temporary-URLs unterstützt — der gebündelte
s3-Treiber tut das. Ein Aufruf von temporaryUrl() auf dem local-Treiber wirft,
sofern Sie keinen Generator dafür registrieren, weil eine lokale Disk nichts zum
Presignen hat.
Wenn Sie den Download lieber auf Ihrer eigenen Route halten möchten — um
Autorisierung pro Anfrage auszuführen oder jeden Zugriff zu loggen —, signieren Sie
stattdessen eine Route mit URL::temporarySignedRoute(). Die signed-Middleware
der Route weist einen manipulierten oder abgelaufenen Link zurück, bevor Ihre Action
läuft.
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
// Mint the link elsewhere:// URL::temporarySignedRoute('reports.download', now()->addMinutes(10),// ['report' => $reportId]);Route::get('/reports/{report}/download', DownloadReportController::class) ->name('reports.download') ->middleware('signed');Codebeispiel — Symfony UriSigner
Abschnitt betitelt „Codebeispiel — Symfony UriSigner“Symfony hat keine Laravel-artige Storage-Fassade, also signieren Sie Ihre eigene
Route mit dem Symfony\Component\HttpFoundation\UriSigner des Frameworks und
lassen diese Route dann zu einer Presigned-Storage-URL weiterleiten (oder das Objekt
streamen). UriSigner::sign() hängt einen geschlüsselten Hash an;
checkRequest() weist einen manipulierten Link zurück. Um das Beispiel über
Symfony-Versionen hinweg portabel zu halten, betten Sie Ihren eigenen
expires-Query-Parameter (einen Unix-Zeitstempel einige Minuten in der Zukunft)
vor dem Signieren ein und validieren diesen Parameter dann selbst in der
download-Route, nachdem die Signatur geprüft wurde. Das funktioniert auf jeder
Symfony-Version, weil UriSigner::sign(string $uri) nur die URL nimmt.
<?php
declare(strict_types=1);
namespace App\Controller;
use NextPDF\Symfony\Service\PdfFactory;use Symfony\Component\HttpFoundation\JsonResponse;use Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\HttpFoundation\UriSigner;use Symfony\Component\Routing\Attribute\Route;use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
final class ReportDeliveryController{ // 1 + 2 + sign: build, store, and return a signed URL to our own route. #[Route('/reports/{reportId}', name: 'report_prepare', methods: ['POST'])] public function prepare( int $reportId, PdfFactory $pdf, UriSigner $signer, UrlGeneratorInterface $urls, ReportStorage $storage, // your storage adapter ): JsonResponse { $document = $pdf->create(); $document->addPage(); $document->cell(0, 10, "Report #{$reportId}", newLine: true);
$key = $storage->put($reportId, $document->getPdfData());
$url = $urls->generate( 'report_download', ['reportId' => $reportId, 'key' => $key], UrlGeneratorInterface::ABSOLUTE_URL, );
// Embed our own expiry (a Unix timestamp 10 minutes out), then sign the // URL only. UriSigner::sign(string $uri) is portable across all versions. $url .= (str_contains($url, '?') ? '&' : '?') . 'expires=' . ((new \DateTimeImmutable('+10 minutes'))->getTimestamp());
return new JsonResponse(['download_url' => $signer->sign($url)]); }
// verify: the signed route. checkRequest() rejects a tampered link; then we // enforce the embedded expiry ourselves. #[Route('/reports/{reportId}/download', name: 'report_download', methods: ['GET'])] public function download( Request $request, UriSigner $signer, ReportStorage $storage, ): Response { if (!$signer->checkRequest($request)) { return new Response('Link invalid.', 403); }
// Enforce the embedded expiry: reject once the timestamp is in the past. $expires = (int) $request->query->get('expires'); if ($expires < time()) { return new Response('Link expired.', 410); }
// Redirect to a presigned storage URL, or stream the object here. return new Response('', 302, ['Location' => $storage->presign( (string) $request->query->get('key'), )]); }}UriSigner wird mit einem Geheimnis konstruiert (Symfony verdrahtet es automatisch
aus dem Parameter %kernel.secret% / APP_SECRET). Das obige Beispiel ist der
portable Pfad: UriSigner::sign(string $uri) signiert nur die URL und existiert auf
jeder Symfony-Version, sodass der Ablauf als Ihr eigener expires-Query-Parameter
mitreist. Die Signatur deckt diesen Parameter ab, sodass er nicht manipuliert werden
kann — und nachdem checkRequest() durchgeht, erzwingt die download-Route ihn,
indem sie den Zeitstempel mit der aktuellen Zeit vergleicht und 410 Gone
zurückgibt, sobald er in der Vergangenheit liegt.
Auf Symfony-Versionen, deren
UriSigner::sign()ein Ablauf-Argument vom TypDateTimeInterfaceakzeptiert, können Sie den Ablauf direkt übergeben —$signer->sign($url, new \DateTimeImmutable('+10 minutes'))— undcheckRequest()abgelaufene Links für Sie zurückweisen lassen, wodurch der manuelleexpires-Parameter und seine Prüfung entfallen. Bestätigen Sie die Signatur vonUriSigner::sign()in Ihrer installierten Symfony-Version, bevor Sie sich darauf verlassen; das portable Muster oben funktioniert unabhängig davon.
Codebeispiel — Cloud-SDK-Presigned-URL
Abschnitt betitelt „Codebeispiel — Cloud-SDK-Presigned-URL“Wenn Sie direkt mit einem Cloud-SDK signieren statt über eine Framework-Disk, ist
die Form dieselbe: das Objekt ablegen, dann das SDK bitten, einen GET dafür zu
presignen. Dies ist reines S3 (der GCS-Ablauf spiegelt es: das Objekt mit
$bucket->object($key) holen und $object->signedUrl($expiresAt, [...]) aufrufen).
<?php
declare(strict_types=1);
use Aws\S3\S3Client;use NextPDF\Core\Document;
/** @var Document $document Already built by your generation code. */$bytes = $document->getPdfData(); // NextPDF: the only engine call.
$s3 = new S3Client(['region' => 'eu-central-1', 'version' => 'latest']);$key = 'reports/' . bin2hex(random_bytes(16)) . '.pdf';
// Store the object privately.$s3->putObject([ 'Bucket' => 'my-private-reports', 'Key' => $key, 'Body' => $bytes, 'ContentType' => 'application/pdf',]);
// Presign a GET valid for 10 minutes. The returned URI is the signed URL.$command = $s3->getCommand('GetObject', [ 'Bucket' => 'my-private-reports', 'Key' => $key,]);$signedUrl = (string) $s3->createPresignedRequest($command, '+10 minutes')->getUri();Für GCS bauen Sie die Bytes auf dieselbe Weise mit getPdfData(), laden das Objekt
mit dem Cloud-Storage-Client hoch, holen dann das Speicherobjekt mit
$bucket->object($key) und rufen $object->signedUrl($expiresAt, [...]) mit einem
Carbon/DateTime-Ablauf auf, um den gleichwertigen Link zu prägen. Der
Signed-URL-Ablauf bei beiden Anbietern ist durch den Anmeldedaten-Typ begrenzt;
ziehen Sie die Dokumentation des Anbieters für die maximale Lebensdauer zurate, die
Ihre Anmeldedaten zulassen.
Grenzfälle & Stolperfallen
Abschnitt betitelt „Grenzfälle & Stolperfallen“- Bauen Sie das Dokument genau einmal.
getPdfData()löst den Build aus, und der Build ist nicht idempotent. Rufen Sie es einmal auf, halten Sie die Zeichenkette und verwenden Sie sie für den Upload und jedeContent-Length, Prüfsumme oderETagwieder, die Sie berechnen. Rufen Sie es nicht erneut auf, um die Bytes „neu zu lesen“. temporaryUrl()braucht einen presign-fähigen Treiber. Laravelss3-Treiber presignt; derlocal-Treiber wirft beitemporaryUrl(), sofern Sie keinen eigenen Generator mitStorage::disk('local')->buildTemporaryUrlsUsing(...)registrieren. Wählen Sie eine Disk, die signieren kann, oder signieren Sie stattdessen eine App-Route.- Setzen Sie den Objekt-Content-Type. Speichern Sie mit
Content-Type: application/pdf(dieContentType-Upload-Option oder Disk-Metadaten), sodass der Browser den Presigned-Link als PDF öffnet, statt einoctet-streamherunterzuladen. - Ein kurzer Ablauf kann einen langsamen Client überdauern. Wenn der Nutzer den Link deutlich nach dem Prägen klickt, kann ein 60-Sekunden-Fenster bereits tot sein. Dimensionieren Sie den Ablauf auf die realistische Lücke zwischen dem Prägen und dem ersten Byte — Minuten, nicht Sekunden — und prägen Sie auf Anforderung neu, statt ihn auf Stunden zu dehnen.
- Eine signierte URL ist Bearer-Zugriff. Jeder, der die URL vor ihrem Ablauf besitzt, kann das Objekt herunterladen. Halten Sie Abläufe kurz, bevorzugen Sie Ein-Objekt-Scope und loggen Sie niemals die vollständige signierte URL — die Signatur ist effektiv ein Token.
- Betten Sie Nutzereingaben nicht unsaniert in den Objekt-Key ein. Bauen Sie
Keys aus Werten, die Sie kontrollieren, plus zufälligen Bytes
(
bin2hex(random_bytes(16))). Ein vorhersehbarer Key lädt zur Enumeration ein, sobald der Bucket auch nur teilweise exponiert ist.
Performance
Abschnitt betitelt „Performance“Dieses Muster tauscht eine synchrone Übertragung gegen einen Upload plus eine winzige JSON-Antwort. Der Anwendungs-Worker wird nur für den PDF-Build und den Upload in den Speicher gehalten, nicht für den vollständigen Download des Clients. Der Download selbst läuft zwischen dem Client und dem Speicher (oder dessen Edge), sodass er überhaupt keinen App-Worker verbraucht.
Der Build ist weiterhin synchron und dominiert weiterhin bei großen oder
mehrseitigen Dokumenten — getPdfData() realisiert das gesamte PDF im Speicher,
bevor Sie es hochladen können. Für schwere Dokumente verschieben Sie Erzeugung und
Upload in einen Queued Job und liefern die signierte URL out of band aus (zum
Beispiel, indem Sie den Client benachrichtigen, wenn das Objekt bereit ist). Siehe
Ein PDF in einem Queued Job erzeugen.
Sicherheitshinweise
Abschnitt betitelt „Sicherheitshinweise“- Halten Sie den Bucket privat; lassen Sie die Signatur den Zugriff gewähren. Machen Sie das Objekt niemals öffentlich lesbar, um die Auslieferung zu „vereinfachen“. Der ganze Sinn ist, dass der Zugriff nur über eine kurzlebige Signatur fließt.
- Kurzer, eingegrenzter Ablauf. Signieren Sie für das kleinste Fenster, das zu Ihrem Ablauf passt, und grenzen Sie jede URL auf ein einzelnes Objekt ein. Ein durchgesickerter Link läuft dann von selbst ab und exponiert nichts anderes.
- Geheimnisse aus der Umgebung. S3-/GCS-Anmeldedaten und das Symfony
APP_SECRET, dasUriSigneruntermauert, kommen aus Umgebungsvariablen oder einem Secret-Manager, niemals aus eingecheckter Konfiguration. Das Rotieren des Signierungsgeheimnisses invalidiert sofort jede ausstehende signierte Route. - Verifizieren Sie vor dem Bedienen auf app-signierten Routen. Wenn der Download
Ihre App durchquert (Laravel
signed-Middleware, SymfonyUriSigner::checkRequest()), verifizieren Sie die Signatur vor jedem Speicherzugriff oder jeder Autorisierung. Weisen Sie einen manipulierten oder abgelaufenen Link mit einem definierten Status zurück. - Loggen Sie niemals die vollständige signierte URL. Die Signatur ist ein Bearer-Credential. Loggen Sie den Objekt-Key und einen Korrelationsbezeichner, nicht die signierte URL, und loggen Sie bei einem Fehler die Exception-Klasse — niemals die Nachricht oder einen Stack-Trace.
- Kein leeres
catch. Jedes Beispiel loggt die Fehlerklasse und gibt eine definierte Fehlerantwort zurück.
Konformität
Abschnitt betitelt „Konformität“Diese Anleitung erhebt keinen normativen Standardanspruch. Der einzige
NextPDF-Engine-AUFRUF, den dieses Auslieferungsmuster benötigt, ist
NextPDF\Core\Document::getPdfData(), die verifizierte öffentliche Methode, die das
rohe PDF-Binary zurückgibt; das Dokument selbst wird so gebaut, wie Ihre App
Dokumente bereits baut (z. B. die injizierte DocumentFactoryInterface / die
Symfony PdfFactory). Die Signatur-Primitive sind dokumentierte Framework- und
Cloud-APIs — Laravel Storage::temporaryUrl() und URL::temporarySignedRoute(),
Symfony UriSigner sowie die S3-/GCS-Presigned-URL-SDK-Operationen —, und ihre
exakten Signaturen, unterstützten Treiber und maximalen Ablauffenster werden von
diesen Upstream-Projekten bestimmt. Ziehen Sie deren Dokumentation für den
maßgeblichen Vertrag auf jeder Plattform zurate.
Siehe auch
Abschnitt betitelt „Siehe auch“- Ein generiertes PDF aus einem Controller zurückgeben — streamen Sie die Bytes direkt, wenn Sie keinen Objektspeicher in der Schleife wollen.
- Ein großes generiertes PDF als HTTP-Antwort streamen — das gepufferte-vs-gestreamte Speichermodell hinter
getPdfData(). - Am Edge mit Cloudflare rendern — die R2-spezifische Signed-URL- und Edge-Render-Variante dieses Musters.
- Ein PDF in einem Queued Job erzeugen — verschieben Sie den Build und Upload vom Request-Thread.