Oluşturulmuş bir PDF'i imzalı, süresi dolan bir URL ile teslim etme
Bir bakışta
“Bir bakışta” başlıklı bölümBir Taşınabilir Belge Biçimi (PDF) dosyası oluşturur ve onu bir istemciye vermeniz gerekir. En basit yol, baytları doğrudan bir denetleyici üzerinden akışla iletir; ama bu, indirmenin tamamı boyunca bir uygulama işçisini meşgul eder, trafiği sunucularınız üzerinden geçirir ve dosyayı, rotaya ulaşabilen herkese açar. Bu sayfadaki teslim örüntüsü tersini yapar: PDF’i oluşturur, baytları nesne depolamasında saklar ve istemcinin doğrudan depolamadan getirdiği kısa ömürlü, imzalı bir Tek Biçimli Kaynak Konum Belirleyicisi (URL) döndürür. Uygulamanız, bir URL içeren küçük bir JavaScript Nesne Gösterimi (JSON) yükü verir; baytları depolama sunar.
NextPDF tarafı tek bir çağrıdır: belgedeki getPdfData(), ham PDF ikilisini bir
dize olarak döndürür. Bundan sonraki her şey — nesneyi koymak ve zaman sınırlı,
imzalı bir bağlantı oluşturmak — çerçevenizin ya da bulut sağlayıcınızın işidir.
İmzalama ilkelleri gerçek, belgelenmiş API’lerdir: Laravel Storage::temporaryUrl()
ve URL::temporarySignedRoute(), Symfony UriSigner ile Amazon Basit Depolama
Hizmeti (S3) ya da Google Cloud Storage (GCS) ön imzalı URL işlemleri, bunların
yazılım geliştirme kitlerinde (SDK’ler). NextPDF kendine ait hiçbir URL yardımcısı
tanımlamaz; öyle bir şey aramayın.
Önce şu parçaları denetleyin:
- NextPDF çekirdeği kurulu ve bir belge oluşturabiliyorsunuz.
- Çerçevenin imzalayabileceği nesne depolamanız var: bir S3 ya da S3 uyumlu paket, bir GCS paketi veya sürücüsü geçici URL’leri destekleyen bir Laravel diski.
- Kimlik bilgileri, gönderime alınmış yapılandırmada değil, ortam değişkenlerinde ya da bir gizli bilgi yöneticisinde yaşar.
Bu bir nasıl yapılır kılavuzudur. Bir isteği bir denetleyiciye nasıl yönlendireceğinizi zaten bildiğiniz varsayılır. Bunun yerine baytları doğrudan döndürmek için bkz. Bir denetleyiciden oluşturulmuş bir PDF döndürme.
Kavramsal genel bakış
“Kavramsal genel bakış” başlıklı bölümÖrüntünün üç adımı vardır ve yalnızca ilki NextPDF’e dokunur:
- Oluştur. Belgeyi oluşturun ve baytları almak için
getPdfData()çağırın. - Sakla. O baytları bir nesne depolama anahtarına yazın (
reports/2026/r-42.pdf). - İmzala. O anahtara, bir son kullanma ile imzalı bir URL’i çerçeveden ya da bulut SDK’sinden isteyin ve URL’i istemciye döndürün.
Baytları geçirmek yerine neden saklayıp imzalamalı:
- Bant genişliğini boşaltın. Nesne depolama (ya da onun içerik dağıtım ağı uç noktası) indirmeyi sunar. Uygulama işçiniz birkaç yüz bayt JSON döndürür ve çok megabaytlık bir aktarımın süresince tutulmak yerine anında serbest kalır.
- Erişimi kapsayın. İmzalı bir URL, bir nesneye, sınırlı bir pencere için erişim verir. Paketin kendisi özel kalır. Kaba kuvvetle aşılacak herkese açık bir rota ve geniş bir paket okuma izni yoktur.
- Son kullanma. İmza, bir son kullanma zaman damgasını gömer. O geçtikten sonra bağlantı ölüdür. Sızdırılmış bir URL kendiliğinden çalışmayı durdurur; bu da kazara bir paylaşımın etki yarıçapını sınırlar.
İki ayrı imzalama modeli vardır ve neyin imzalandığı bakımından farklılık gösterirler:
- Nesne depolama ön imzalı URL’leri (S3, GCS ya da bir S3/GCS diski üzerinden
Laravel’in
temporaryUrl()çağrısı), doğrudan depolama nesnesini işaret eder. İndirme uygulamanıza hiç ulaşmaz. - Uygulama imzalı rotaları (Laravel
URL::temporarySignedRoute(), SymfonyUriSigner), kendi rotanızı işaret eder. İstek yine de uygulamanıza çarpar; uygulama imzayı doğrular, sonra nesneye akış sağlar ya da yönlendirir. Her indirmede yetkilendirme, günlük tutma ya da muhasebe çalıştırmanız gerektiğinde veya depolamanız ön imzalayamadığında bunları kullanın.
API yüzeyi
“API yüzeyi” başlıklı bölüm| İlgi alanı | NextPDF | Laravel | Symfony |
|---|---|---|---|
| PDF baytlarını al | NextPDF\Core\Document::getPdfData(): string | aynı | aynı |
| Baytları sakla | — | Storage::disk($d)->put($key, $bytes) | Filesystem::dumpFile($path, $bytes) veya Flysystem write() |
| Ön imzalı depolama URL’i | — | Storage::disk($d)->temporaryUrl($key, $expiresAt) | AWS/GCS SDK ön imzalayıcısı (aşağıda) |
| İmzalı uygulama rotası | — | URL::temporarySignedRoute($name, $expiresAt, $params) | UriSigner::sign($url) |
| İmzalı bir uygulama rotasını doğrula | — | signed rota ara yazılımı / $request->hasValidSignature() | UriSigner::check() / checkRequest() |
Bu teslim örüntüsünün gerektirdiği tek NextPDF motor ÇAĞRISI getPdfData()’dır;
belgenin kendisi, uygulamanızın belgeleri zaten nasıl oluşturduğuna göre
oluşturulur (ör. enjekte edilen DocumentFactoryInterface / Symfony PdfFactory).
getPdfData(), NextPDF\Core\Document üzerindeki HasOutput trait’inde
bildirilir. Yazıcıyı bir kez çağırır ve PDF’in tamamını bir dize olarak döndürür.
Kardeşi save(string $path): void, aynı baytları atomik bir yazıcı aracılığıyla
diske yazar; onu yalnızca depolamanız gerçek bir yerel dosya sistemi yolu
olduğunda kullanın. Nesne depolaması için, getPdfData()’yı tercih edin ve
aktarımın sahipliğini depolama SDK’sine bırakın.
Belge,
getPdfData()(ya dasave()) çağırdığınızda oluşturulur ve oluşturma bağımsız (idempotent) değildir. Onu belge başına bir kez çağırın, dizeyi yakalayın ve o dizeyi hem yükleme hem de hesapladığınız herhangi bir boyut ya da sağlama toplamı için yeniden kullanın.
Kod örneği — Laravel geçici URL
“Kod örneği — Laravel geçici URL” başlıklı bölümLaravel’in dosya sistemi soyutlaması sizin için imzalar. Bir S3 (ya da S3 uyumlu)
diskinde, Storage::temporaryUrl(), doğrudan nesneye giden ön imzalı bir URL
döndürür. İstemci depolamadan indirir; eyleminiz yalnızca JSON döndürür.
<?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); } }}Disk, sürücüsü geçici URL’leri destekleyen bir disk olmalıdır — paketlenmiş s3
sürücüsü destekler. local sürücüsünde temporaryUrl() çağırmak, onun için bir
üretici kaydetmediğiniz sürece istisna fırlatır; çünkü yerel bir diskin ön
imzalayacak bir şeyi yoktur.
İndirmeyi kendi rotanızda tutmayı tercih ettiğinizde — her istek başına
yetkilendirme çalıştırmak ya da her erişimi günlüğe yazmak için — bunun yerine
URL::temporarySignedRoute() ile bir rotayı imzalayın. Rotanın signed ara
yazılımı, kurcalanmış ya da süresi dolmuş bir bağlantıyı, eyleminiz çalışmadan
önce reddeder.
<?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');Kod örneği — Symfony UriSigner
“Kod örneği — Symfony UriSigner” başlıklı bölümSymfony’nin Laravel tarzı bir depolama cephesi yoktur; bu nedenle kendi
rotanızı çerçevenin Symfony\Component\HttpFoundation\UriSigner’ıyla
imzalarsınız, sonra o rotayı ön imzalı bir depolama URL’ine yönlendirirsiniz (ya
da nesneye akış sağlarsınız). UriSigner::sign() anahtarlı bir karma ekler;
checkRequest() kurcalanmış bir bağlantıyı reddeder. Örneği Symfony sürümleri
arasında taşınabilir tutmak için, imzalamadan önce kendi expires sorgu
parametrenizi (birkaç dakika ileride bir Unix zaman damgası) gömün, sonra imza
doğrulandıktan sonra download rotasında o parametreyi kendiniz doğrulayın. Bu,
her Symfony sürümünde çalışır; çünkü UriSigner::sign(string $uri) yalnızca
URL’i alır.
<?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, bir gizli anahtarla kurulur (Symfony onu %kernel.secret% /
APP_SECRET parametresinden otomatik bağlar). Yukarıdaki örnek, taşınabilir
yoldur: UriSigner::sign(string $uri), yalnızca URL’i imzalar ve her Symfony
sürümünde bulunur; bu nedenle son kullanma, kendi expires sorgu parametreniz
olarak seyahat eder. İmza o parametreyi kapsar; bu yüzden kurcalanamaz — ve
checkRequest() geçtikten sonra, download rotası zaman damgasını geçerli zamanla
karşılaştırarak ve damga geçmişte kaldığında 410 Gone döndürerek onu uygular.
UriSigner::sign()’ı bir son kullanmaDateTimeInterfacebağımsız değişkeni kabul eden Symfony sürümlerinde, son kullanmayı doğrudan geçirebilir —$signer->sign($url, new \DateTimeImmutable('+10 minutes'))— ve süresi dolmuş bağlantıları sizin yerinizecheckRequest()’in reddetmesine izin vererek elleexpiresparametresini ve denetimini bırakabilirsiniz. Ona güvenmeden önce kurulu Symfony’nizdekiUriSigner::sign()imzasını onaylayın; yukarıdaki taşınabilir örüntü ne olursa olsun çalışır.
Kod örneği — Bulut SDK ön imzalı URL
“Kod örneği — Bulut SDK ön imzalı URL” başlıklı bölümBir çerçeve diski yerine doğrudan bir bulut SDK’siyle imzalarsanız, biçim aynıdır:
nesneyi koyun, sonra SDK’den onun için bir GET ön imzalamasını isteyin. Bu, düz
S3’tür (GCS akışı bunu yansıtır: nesneyi $bucket->object($key) ile alın ve
$object->signedUrl($expiresAt, [...]) çağırın).
<?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();GCS için, baytları aynı şekilde getPdfData() ile oluşturun, nesneyi Cloud
Storage istemcisiyle yükleyin, sonra depolama nesnesini $bucket->object($key)
ile alın ve eşdeğer bağlantıyı oluşturmak için bir Carbon/DateTime son
kullanmasıyla $object->signedUrl($expiresAt, [...]) çağırın. Her iki
sağlayıcıda da imzalı URL son kullanması, kimlik bilgisi türüyle sınırlıdır;
kimlik bilgilerinizin izin verdiği en uzun ömür için sağlayıcının belgelerine
başvurun.
Uç durumlar ve dikkat edilecek noktalar
“Uç durumlar ve dikkat edilecek noktalar” başlıklı bölüm- Belgeyi tam olarak bir kez oluşturun.
getPdfData()oluşturmayı tetikler ve oluşturma bağımsız değildir. Onu bir kez çağırın, dizeyi tutun ve hem yükleme hem de hesapladığınız herhangi birContent-Length, sağlama toplamı ya daETagiçin onu yeniden kullanın. Baytları “yeniden okumak” için onu tekrar çağırmayın. temporaryUrl()ön imzalama yetenekli bir sürücü gerektirir. Laravel’ins3sürücüsü ön imzalar;localsürücüsü,Storage::disk('local')->buildTemporaryUrlsUsing(...)ile özel bir üretici kaydetmediğiniz sürecetemporaryUrl()’de istisna fırlatır. İmzalayabilen bir disk seçin ya da bunun yerine bir uygulama rotasını imzalayın.- Nesne içerik türünü ayarlayın. Tarayıcının ön imzalı bağlantıyı bir
octet-streamolarak indirmek yerine bir PDF olarak açması içinContent-Type: application/pdfile saklayın (ContentTypeyükleme seçeneği ya da disk üst verisi). - Kısa bir son kullanma, yavaş bir istemciden daha kısa ömürlü olabilir. Kullanıcı bağlantıya, onu oluşturduktan epey sonra tıklarsa, 60 saniyelik bir pencere çoktan ölü olabilir. Son kullanmayı, oluşturma ile ilk bayt arasındaki gerçekçi boşluğa göre boyutlandırın — saniyeler değil, dakikalar — ve onu saatlere uzatmak yerine istek üzerine yeniden oluşturun.
- İmzalı bir URL hamiline erişimdir. URL’i, süresi dolmadan önce elinde tutan herkes nesneyi indirebilir. Son kullanmaları kısa tutun, tek nesne kapsamını tercih edin ve tam imzalı URL’i asla günlüğe yazmayın — imza, etkin olarak bir belirteçtir.
- Kullanıcı girdisini nesne anahtarına temizlemeden gömmeyin. Anahtarları,
denetlediğiniz değerler artı rastgele baytlardan oluşturun
(
bin2hex(random_bytes(16))). Öngörülebilir bir anahtar, paket kısmen bile açığa çıktığında numaralandırmayı davet eder.
Performans
“Performans” başlıklı bölümBu örüntü, bir eşzamanlı aktarımı bir yükleme artı küçük bir JSON yanıtıyla takas eder. Uygulama işçisi yalnızca PDF oluşturma ve depolamaya yükleme için tutulur, istemcinin tam indirmesi için değil. İndirmenin kendisi istemci ile depolama (ya da onun uç noktası) arasında akar; bu nedenle hiç uygulama işçisi tüketmez.
Oluşturma yine de eşzamanlıdır ve büyük ya da çok sayfalı belgelerde yine baskındır
— getPdfData(), yükleyebilmenizden önce PDF’in tamamını bellekte gerçekler. Ağır
belgeler için, oluşturma ile yüklemeyi kuyruğa alınmış bir işe taşıyın ve imzalı
URL’i bant dışı teslim edin (örneğin nesne hazır olduğunda istemciyi bildirerek).
Bkz. Kuyruğa alınmış bir işte PDF oluşturma.
Güvenlik notları
“Güvenlik notları” başlıklı bölüm- Paketi özel tutun; erişimi imza versin. Teslimi “basitleştirmek” için nesneyi asla herkese açık olarak okunabilir yapmayın. Bütün mesele, erişimin yalnızca kısa ömürlü bir imza üzerinden akmasıdır.
- Kısa, kapsamlı son kullanma. Akışınıza uyan en küçük pencere için imzalayın ve her URL’i tek bir nesneye kapsayın. Sızdırılmış bir bağlantı o zaman kendiliğinden dolar ve başka hiçbir şeyi açığa çıkarmaz.
- Gizli bilgiler ortamdan. S3/GCS kimlik bilgileri ve
UriSigner’ı destekleyen SymfonyAPP_SECRET, gönderime alınmış yapılandırmadan değil, ortam değişkenlerinden ya da bir gizli bilgi yöneticisinden gelir. İmzalama gizli anahtarını döndürmek, bekleyen her imzalı rotayı anında geçersiz kılar. - Uygulama imzalı rotalarda hizmet vermeden önce doğrulayın. İndirme
uygulamanızı geçtiğinde (Laravel
signedara yazılımı, SymfonyUriSigner::checkRequest()), imzayı herhangi bir depolama erişiminden ya da yetkilendirmeden önce doğrulayın. Kurcalanmış ya da süresi dolmuş bir bağlantıyı tanımlı bir durumla reddedin. - Tam imzalı URL’i asla günlüğe yazmayın. İmza, hamiline bir kimlik bilgisidir. İmzalı URL’i değil, nesne anahtarını ve bir ilişkilendirme tanımlayıcısını günlüğe yazın ve başarısızlıkta istisna sınıfını günlüğe yazın — iletiyi ya da bir yığın izlemesini asla.
- Boş
catchyok. Her örnek, başarısızlık sınıfını günlüğe yazar ve tanımlı bir hata yanıtı döndürür.
Uygunluk
“Uygunluk” başlıklı bölümBu kılavuz hiçbir normatif standart iddiasında bulunmaz. Bu teslim örüntüsünün
gerektirdiği tek NextPDF motor ÇAĞRISI, ham PDF ikilisini döndüren doğrulanmış
genel yöntem NextPDF\Core\Document::getPdfData()’dır; belgenin kendisi,
uygulamanızın belgeleri zaten nasıl oluşturduğuna göre oluşturulur (ör. enjekte
edilen DocumentFactoryInterface / Symfony PdfFactory). İmzalama ilkelleri
belgelenmiş çerçeve ve bulut API’leridir — Laravel Storage::temporaryUrl() ve
URL::temporarySignedRoute(), Symfony UriSigner ile S3/GCS ön imzalı URL SDK
işlemleri — ve bunların tam imzaları, desteklenen sürücüleri ve en uzun son
kullanma pencereleri o üst akış projeleri tarafından yönetilir. Her platformdaki
yetkili sözleşme için belgelerine başvurun.
Ayrıca bkz.
“Ayrıca bkz.” başlıklı bölüm- Bir denetleyiciden oluşturulmuş bir PDF döndürme — döngüde nesne depolaması istemediğinizde baytları doğrudan akışla iletin.
- Büyük, oluşturulmuş bir PDF’i HTTP yanıtı olarak akışla iletme —
getPdfData()’nın arkasındaki arabelleğe alınmış vs. akışla iletilen bellek modeli. - Cloudflare ile uçta işleme — bu örüntünün R2’ye özgü imzalı URL ve uçta işleme varyantı.
- Kuyruğa alınmış bir işte PDF oluşturma — oluşturma ile yüklemeyi istek iş parçacığından çıkarın.