Проблема: рахунки, договори, вкладення листування лежать у сховищі, а посилання на них мають вигляд /storage/invoices/1041.pdf. Хто знає чи вгадає адресу, той завантажить файл - без перевірки прав.
Перше правило: приватні файли - не в публічній папці (public/, storage/app/public з посиланням storage:link), а на приватному диску. Віддає їх контролер з перевіркою прав:
Route::get('/invoices/{invoice}/download', function (Invoice $invoice) {
Gate::authorize('download', $invoice);
return Storage::disk('private')->download($invoice->path, "invoice-{$invoice->number}.pdf");
})->middleware('auth');
Підписані URL - коли файл треба віддати без входу (посилання в листі, вбудування в сторонній сервіс) або на обмежений час:
$url = URL::temporarySignedRoute(
'invoices.download',
now()->plus(hours: 24),
['invoice' => $invoice->id],
);
Route::get('/invoices/{invoice}/download', DownloadInvoiceController::class)
->name('invoices.download')
->middleware('signed');
- до URL додаються
expiresіsignature- HMAC від усієї адреси з ключем застосунку (APP_KEY); - зміна будь-якого параметра (
invoice=1042) чи терміну дії робить підпис недійсним -403; - після закінчення терміну посилання перестає працювати.
Що важливо розуміти:
- підписаний URL - це «пред'явницький» доступ: будь-хто з посиланням отримає файл. Переслане, збережене в історії, потрапило в лог - доступ має кожен. Тому: короткий термін дії, а для чутливих документів - підпис плюс перевірка входу й прав;
- ключ підпису -
APP_KEY: його зміна робить недійсними всі видані посилання; - за проксі (Cloudflare, балансувальник) адреса, яку бачить Laravel, може відрізнятися від підписаної (схема, хост) - тоді допомагає
signed:relativeі підпис відносного URL (absolute: false); - одноразовість підписані URL не забезпечують - для одноразових посилань потрібна позначка в базі.
Для файлів у S3/R2 аналог - Storage::temporaryUrl(): підписане посилання сховища, і файл віддається напряму з нього, минаючи сервер застосунку.