Реалізація генерації унікальних посилань на завантаження

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація генерації унікальних посилань на завантаження
Середня
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація генерації унікальних посилань на завантаження

Унікальне посилання — основний механізм контролю доступу до цифрових товарів. На відміну від прямого посилання на файл, унікальний токен не дозволяє угадати URL іншої покупки та прив'язує завантаження до конкретної транзакції. Правильна реалізація потребує криптографічно стійкого генератора, захисту від перебору та механізму ротації скомпрометованих посилань.

Вимоги до токена

  • Непередбачуваність — неможливо угадати або вивести з порядкового номера замовлення
  • Достатня ентропія — мінімум 128 бітів (32 байти випадкових даних → 64 символи hex)
  • Унікальність — в таблиці UNIQUE-обмеження з обробкою колізій
  • Короткий URL — токен не повинен перевищувати 64–80 символів

Генерація токена

class DownloadTokenGenerator
{
    /**
     * Генеруємо 32 випадкові байти через CSPRNG та конвертуємо в hex
     * random_bytes() використовує /dev/urandom на Linux, CryptGenRandom на Windows
     */
    public function generate(): string
    {
        return bin2hex(random_bytes(32)); // 64 символи
    }

    /**
     * Генерація з гарантією унікальності
     */
    public function generateUnique(int $maxAttempts = 5): string
    {
        for ($i = 0; $i < $maxAttempts; $i++) {
            $token = $this->generate();

            if (!DigitalOrderDownload::where('token', $token)->exists()) {
                return $token;
            }
        }

        // Колізія 5 разів підряд при 64-символьному hex — статистично неможлива,
        // але обробляємо як hard error
        throw new TokenGenerationFailedException('Не вдалося сгенерувати унікальний токен');
    }
}

Короткі токени (опціонально)

Якщо потрібні читабельні посилання (для SMS, QR-кодів), використовуємо Base62:

class ShortTokenGenerator
{
    private const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

    /**
     * 16 символів Base62 = ~95 бітів ентропії — достатньо для посилань завантаження
     */
    public function generate(int $length = 16): string
    {
        $token = '';
        $bytes = random_bytes($length);

        for ($i = 0; $i < $length; $i++) {
            $token .= self::ALPHABET[ord($bytes[$i]) % 62];
        }

        return $token;
    }
}

Структура URL

https://example.com/dl/a3f8c92b1d4e7f0a2b5c8d9e1f3g4h5j

Короткий шлях /dl/ — не /download/ — щоб не розкривати логіку системи. Токен в шляху, не в query string — query string логується в access_log та може потрапити в рефереру.

// routes/web.php
Route::get('/dl/{token}', [DigitalDownloadController::class, 'show'])
    ->name('digital.download.show')
    ->middleware(['throttle:30,1']); // максимум 30 запитів на хвилину

Route::get('/dl/{token}/get', [DigitalDownloadController::class, 'download'])
    ->name('digital.download.get')
    ->middleware(['throttle:10,1']); // реальне завантаження — жорсткіше

Rate limiting проти перебору

// app/Http/Middleware/ThrottleDownloadTokens.php
class ThrottleDownloadAttempts
{
    public function handle(Request $request, Closure $next): Response
    {
        $key = 'download-attempt:' . $request->ip();

        if (RateLimiter::tooManyAttempts($key, 20)) {
            // Забагато спроб з однієї IP
            abort(429, 'Забагато запитів. Спробуйте пізніше.');
        }

        RateLimiter::hit($key, 60); // вікно 1 хвилина

        $response = $next($request);

        // Неверний токен (404) також вважається спробою
        if ($response->getStatusCode() === 404) {
            RateLimiter::hit('invalid-token:' . $request->ip(), 3600);

            if (RateLimiter::tooManyAttempts('invalid-token:' . $request->ip(), 10)) {
                // Більше 10 неіснуючих токенів з однієї IP за годину — блокуємо
                app(IpBlocklistService::class)->block($request->ip(), reason: 'token_bruteforce');
            }
        }

        return $response;
    }
}

Ротація скомпрометованого посилання

Якщо покупець повідомляє, що посилання було опубліковано або вкрадено:

class RotateDownloadTokenAction
{
    public function execute(DigitalOrderDownload $download, string $reason): DigitalOrderDownload
    {
        // Інвалідуємо старий токен
        $download->update(['is_revoked' => true]);

        // Логуємо причину
        DownloadTokenRotationLog::create([
            'original_token' => $download->token,
            'reason'         => $reason,
            'rotated_at'     => now(),
            'rotated_by'     => auth()->id(),
        ]);

        // Створюємо новий токен з тими ж параметрами
        $newDownload = DigitalOrderDownload::create([
            'order_item_id'      => $download->order_item_id,
            'digital_product_id' => $download->digital_product_id,
            'token'              => app(DownloadTokenGenerator::class)->generateUnique(),
            'downloads_count'    => 0, // лічильник скидається
            'downloads_limit'    => $download->downloads_limit,
            'expires_at'         => $download->expires_at,
        ]);

        // Надсилаємо нове посилання
        Mail::to($download->orderItem->order->email)
            ->send(new DownloadTokenRotatedMail($newDownload));

        return $newDownload;
    }
}

Підписані URL (альтернативний підхід)

Замість зберігання токена в БД можна використовувати HMAC-підписані URL. Перевага — не потрібна запис в БД для кожної покупки. Недостаток — неможливо інвалідувати окремий токен без ключа.

// Генерація підписаного URL з закінченням
$url = URL::temporarySignedRoute(
    'digital.download.get',
    now()->addDays(30),
    ['order_item_id' => $item->id]
);

// Перевірка в контролері — автоматично через SignedMiddleware
Route::get('/dl/signed/{order_item_id}', [DigitalDownloadController::class, 'signedDownload'])
    ->name('digital.download.get')
    ->middleware('signed');

Підписані URL використовуються, коли потрібно дати доступ багатьом користувачам одразу (наприклад, корпоративна ліцензія на 100 співробітників) без створення 100 записів в БД.

Моніторинг аномалій

// Алерт: один токен завантажується з множини IP
$suspiciousDownloads = DownloadEvent::selectRaw('
    digital_order_download_id,
    COUNT(DISTINCT ip_address) as unique_ips,
    COUNT(*) as total_downloads
')
->where('downloaded_at', '>', now()->subHours(24))
->groupBy('digital_order_download_id')
->having('unique_ips', '>', 5)
->get();

Строки

Базова генерація токенів + захист від перебору — 1–2 робочих дні. Ротація токенів, моніторинг аномалій, підписані URL як альтернативний механізм — ще 2 дні.