İçeriğe geç
getnextpdf.com

Oluşturulmuş bir PDF'i imzalı, süresi dolan bir URL ile teslim etme

Bir 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.

Örüntünün üç adımı vardır ve yalnızca ilki NextPDF’e dokunur:

  1. Oluştur. Belgeyi oluşturun ve baytları almak için getPdfData() çağırın.
  2. Sakla. O baytları bir nesne depolama anahtarına yazın (reports/2026/r-42.pdf).
  3. İ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(), Symfony UriSigner), 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.
İlgi alanıNextPDFLaravelSymfony
PDF baytlarını alNextPDF\Core\Document::getPdfData(): stringaynıaynı
Baytları saklaStorage::disk($d)->put($key, $bytes)Filesystem::dumpFile($path, $bytes) veya Flysystem write()
Ön imzalı depolama URL’iStorage::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ğrulasigned 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 da save()) ç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.

Laravel’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.

app/Http/Controllers/ReportDeliveryController.php
<?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.

routes/web.php
<?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');

Symfony’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.

src/Controller/ReportDeliveryController.php
<?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 kullanma DateTimeInterface bağı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 yerinize checkRequest()’in reddetmesine izin vererek elle expires parametresini ve denetimini bırakabilirsiniz. Ona güvenmeden önce kurulu Symfony’nizdeki UriSigner::sign() imzasını onaylayın; yukarıdaki taşınabilir örüntü ne olursa olsun çalışır.

Bir ç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).

store-and-presign.php
<?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.

  • 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 bir Content-Length, sağlama toplamı ya da ETag iç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’in s3 sürücüsü ön imzalar; local sürücüsü, Storage::disk('local')->buildTemporaryUrlsUsing(...) ile özel bir üretici kaydetmediğiniz sürece temporaryUrl()’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-stream olarak indirmek yerine bir PDF olarak açması için Content-Type: application/pdf ile saklayın (ContentType yü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.

Bu ö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.

  • 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 Symfony APP_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 signed ara yazılımı, Symfony UriSigner::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ş catch yok. 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.

Bu 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.