· Nacho Coll · Guides  · 10 хв читання

Токени завантаження IPFS: безпечне завантаження на стороні клієнта без розкриття API-ключів

Дізнайтеся, як підписані токени завантаження дозволяють безпечно завантажувати файли в IPFS з браузерів та мобільних додатків, не розкриваючи ваш API-ключ.

Дізнайтеся, як підписані токени завантаження дозволяють безпечно завантажувати файли в IPFS з браузерів та мобільних додатків, не розкриваючи ваш API-ключ.

Розробка сучасних веб-додатків часто вимагає завантаження файлів безпосередньо з браузерів користувачів у хмарне сховище. Однак, коли йдеться про безпеку завантаження в IPFS, розробники стикаються зі складною дилемою: як дозволити завантаження на стороні клієнта, не розкриваючи цінні API-ключі для потенційного зловживання?

Більшість сервісів закріплення IPFS ставлять вас перед незручним вибором: або обробляти всі завантаження на стороні сервера (створюючи вузькі місця та ускладнюючи архітектуру), або вбудовувати API-ключ у клієнтський код (що є кошмаром з точки зору безпеки). IPFS.NINJA вирішує цю проблему за допомогою унікальної функції, яку не пропонує жоден інший сервіс закріплення: підписані токени завантаження.

IPFS Ninja

Проблема традиційних API-ключів IPFS

При створенні клієнтських додатків, яким потрібно завантажувати файли в IPFS, розробники зазвичай стикаються з кількома проблемами безпеки:

Ризик розкриття API-ключа

Вбудовування API-ключів безпосередньо в JavaScript браузера означає, що будь-хто може переглянути ваш вихідний код і витягти облікові дані. Це може призвести до:

  • Несанкціонованих завантажень, що витрачають вашу квоту сховища
  • Потенційного зловживання вашим обліковим записом сервісу закріплення
  • Порушень вимог безпеки в корпоративних середовищах

Вузькі місця на стороні сервера

Альтернатива — маршрутизація всіх завантажень через ваш бекенд — створює низку проблем:

  • Збільшення витрат на серверну пропускну здатність
  • Підвищена затримка для користувачів
  • Складніші вимоги до інфраструктури
  • Потенційні єдині точки відмови

Безпека мобільних додатків

Мобільні додатки стикаються з аналогічними проблемами: API-ключі, що зберігаються в пакетах додатків, можуть бути витягнуті шляхом зворотної розробки.

Представляємо токени завантаження IPFS

Підписані токени завантаження IPFS.NINJA забезпечують безпечний компроміс. Ось як вони працюють:

  1. Сервер генерує токен: ваш бекенд створює обмежений у часі підписаний токен за допомогою вашого API-ключа
  2. Клієнт отримує токен: токен безпечно передається вашому фронтенд-додатку
  3. Пряме завантаження: клієнти завантажують файли безпосередньо в IPFS.NINJA, використовуючи підписаний токен
  4. Автоматичне закінчення терміну дії: токени закінчуються після заданого часу, обмежуючи вікно вразливості

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

Розуміння безпеки токенів завантаження

Підписані токени завантаження використовують криптографічні підписи для забезпечення автентичності без розкриття вашого основного API-ключа. Кожен токен містить:

  • Часова мітка закінчення: автоматична анулювання після вказаного періоду
  • Обмеження використання: опціональні ліміти на кількість файлів або загальний розмір
  • Криптографічний підпис: запобігає підробці або фальсифікації
  • Верифікація видавця: прив’язка до вашого автентифікованого облікового запису

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

Реалізація на бекенді: приклад на Express.js

Давайте створимо повний приклад, що демонструє реалізацію безпечного клієнтського завантаження в IPFS. Спочатку розглянемо бекенд на Express.js, що генерує токени завантаження:

// server.js
const express = require('express');
const cors = require('cors');
const app = express();

app.use(express.json());
app.use(cors());

// Your IPFS.NINJA API key (keep this secure on server-side only)
const IPFS_API_KEY = 'bws_1234567890abcdef1234567890abcdef12345678';

// Generate a signed upload token
app.post('/api/generate-upload-token', async (req, res) => {
  try {
    const response = await fetch('https://api.ipfs.ninja/upload-tokens', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Api-Key': IPFS_API_KEY
      },
      body: JSON.stringify({
        expiresIn: '1h', // Token valid for 1 hour
        maxUploads: 10,  // Optional: limit number of uploads
        maxSizeMB: 50    // Optional: limit total upload size
      })
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const tokenData = await response.json();
    
    res.json({
      success: true,
      uploadToken: tokenData.token,
      expiresAt: tokenData.expiresAt
    });
  } catch (error) {
    console.error('Token generation failed:', error);
    res.status(500).json({
      success: false,
      error: 'Failed to generate upload token'
    });
  }
});

// Optional: Endpoint to verify uploads completed successfully
app.post('/api/verify-upload', async (req, res) => {
  const { cid } = req.body;
  
  try {
    // Verify the file was pinned successfully
    const response = await fetch(`https://api.ipfs.ninja/pins/${cid}`, {
      headers: {
        'X-Api-Key': IPFS_API_KEY
      }
    });

    const pinData = await response.json();
    
    res.json({
      success: true,
      verified: pinData.pinned,
      metadata: pinData
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: 'Verification failed'
    });
  }
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Реалізація на фронтенді: безпечне клієнтське завантаження

Тепер розглянемо фронтенд-код, який безпечно завантажує файли за допомогою підписаного токена:

<!DOCTYPE html>
<html>
<head>
    <title>Secure IPFS Upload Demo</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
        .upload-area { border: 2px dashed #ccc; padding: 20px; text-align: center; margin: 20px 0; }
        .upload-area.dragover { border-color: #007cba; background: #f0f8ff; }
        button { background: #007cba; color: white; border: none; padding: 10px 20px; cursor: pointer; }
        .status { margin: 10px 0; padding: 10px; border-radius: 4px; }
        .success { background: #d4edda; color: #155724; }
        .error { background: #f8d7da; color: #721c24; }
        .info { background: #d1ecf1; color: #0c5460; }
    </style>
</head>
<body>
    <h1>Secure IPFS Upload with Signed Tokens</h1>
    
    <div class="upload-area" id="uploadArea">
        <p>Drag & drop files here or click to select</p>
        <input type="file" id="fileInput" multiple style="display: none;">
        <button onclick="document.getElementById('fileInput').click()">Select Files</button>
    </div>

    <div id="status"></div>
    <div id="results"></div>

    <script>
        class SecureIPFSUploader {
            constructor() {
                this.uploadToken = null;
                this.tokenExpiry = null;
                this.setupEventListeners();
            }

            setupEventListeners() {
                const uploadArea = document.getElementById('uploadArea');
                const fileInput = document.getElementById('fileInput');

                // Drag and drop handlers
                uploadArea.addEventListener('dragover', (e) => {
                    e.preventDefault();
                    uploadArea.classList.add('dragover');
                });

                uploadArea.addEventListener('dragleave', () => {
                    uploadArea.classList.remove('dragover');
                });

                uploadArea.addEventListener('drop', (e) => {
                    e.preventDefault();
                    uploadArea.classList.remove('dragover');
                    this.handleFiles(Array.from(e.dataTransfer.files));
                });

                // File input handler
                fileInput.addEventListener('change', (e) => {
                    this.handleFiles(Array.from(e.target.files));
                });
            }

            async getUploadToken() {
                // Check if we have a valid token
                if (this.uploadToken && this.tokenExpiry && new Date() < new Date(this.tokenExpiry)) {
                    return this.uploadToken;
                }

                try {
                    this.showStatus('Generating secure upload token...', 'info');
                    
                    const response = await fetch('/api/generate-upload-token', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        }
                    });

                    if (!response.ok) {
                        throw new Error(`Failed to generate token: ${response.statusText}`);
                    }

                    const data = await response.json();
                    
                    if (!data.success) {
                        throw new Error(data.error || 'Token generation failed');
                    }

                    this.uploadToken = data.uploadToken;
                    this.tokenExpiry = data.expiresAt;
                    
                    return this.uploadToken;
                } catch (error) {
                    this.showStatus(`Token generation failed: ${error.message}`, 'error');
                    throw error;
                }
            }

            async uploadFile(file) {
                try {
                    const token = await this.getUploadToken();
                    
                    // Convert file to base64 for JSON transport
                    const fileBase64 = await this.fileToBase64(file);
                    
                    this.showStatus(`Uploading ${file.name} to IPFS...`, 'info');

                    const response = await fetch('https://api.ipfs.ninja/upload/new', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'Authorization': `Signed ${token}`
                        },
                        body: JSON.stringify({
                            content: fileBase64,
                            description: `File uploaded via secure token: ${file.name}`,
                            metadata: {
                                filename: file.name,
                                fileType: file.type,
                                uploadedAt: new Date().toISOString(),
                                uploadMethod: 'signed-token'
                            }
                        })
                    });

                    if (!response.ok) {
                        const errorText = await response.text();
                        throw new Error(`Upload failed: ${response.status} ${errorText}`);
                    }

                    const result = await response.json();
                    
                    return {
                        success: true,
                        filename: file.name,
                        cid: result.cid,
                        size: result.sizeMB,
                        ipfsUri: result.uris.ipfs,
                        httpUrl: result.uris.url
                    };
                } catch (error) {
                    return {
                        success: false,
                        filename: file.name,
                        error: error.message
                    };
                }
            }

            async handleFiles(files) {
                if (files.length === 0) return;

                this.clearResults();
                
                try {
                    // Upload files concurrently
                    const uploadPromises = files.map(file => this.uploadFile(file));
                    const results = await Promise.all(uploadPromises);
                    
                    this.displayResults(results);
                    
                    const successful = results.filter(r => r.success).length;
                    const total = results.length;
                    
                    if (successful === total) {
                        this.showStatus(`✅ Successfully uploaded ${successful} file(s) to IPFS!`, 'success');
                    } else {
                        this.showStatus(`⚠️ Uploaded ${successful}/${total} files. Check results below.`, 'error');
                    }
                } catch (error) {
                    this.showStatus(`Upload failed: ${error.message}`, 'error');
                }
            }

            fileToBase64(file) {
                return new Promise((resolve, reject) => {
                    const reader = new FileReader();
                    reader.readAsDataURL(file);
                    reader.onload = () => {
                        // Remove the data:mime/type;base64, prefix
                        const base64 = reader.result.split(',')[1];
                        resolve(base64);
                    };
                    reader.onerror = error => reject(error);
                });
            }

            showStatus(message, type) {
                const statusDiv = document.getElementById('status');
                statusDiv.className = `status ${type}`;
                statusDiv.textContent = message;
            }

            displayResults(results) {
                const resultsDiv = document.getElementById('results');
                
                resultsDiv.innerHTML = '<h3>Upload Results:</h3>' + 
                    results.map(result => `
                        <div class="status ${result.success ? 'success' : 'error'}" style="margin: 10px 0;">
                            <strong>${result.filename}</strong><br>
                            ${result.success ? 
                                `✅ CID: ${result.cid}<br>
                                 📊 Size: ${result.size} MB<br>
                                 🔗 URL: <a href="${result.httpUrl}" target="_blank">${result.httpUrl}</a>` :
                                `❌ Error: ${result.error}`
                            }
                        </div>
                    `).join('');
            }

            clearResults() {
                document.getElementById('results').innerHTML = '';
            }
        }

        // Initialize the uploader
        const uploader = new SecureIPFSUploader();
    </script>
</body>
</html>

Розширені аспекти безпеки

При реалізації безпеки завантаження в IPFS з підписаними токенами враховуйте наступні додаткові заходи:

Обмеження області дії токена

Налаштовуйте токени з відповідними обмеженнями:

// Generate token with specific constraints
const restrictedToken = await fetch('https://api.ipfs.ninja/upload-tokens', {
  method: 'POST',
  headers: {
    'X-Api-Key': IPFS_API_KEY
  },
  body: JSON.stringify({
    expiresIn: '30m',        // Short expiration
    maxUploads: 5,           // Limited upload count
    maxSizeMB: 10,          // Size restriction
    allowedMimeTypes: ['image/jpeg', 'image/png'], // File type restrictions
    ipWhitelist: ['192.168.1.0/24'] // IP-based access control
  })
});

Валідація контенту

Завжди перевіряйте завантажений контент на вашому бекенді:

app.post('/api/validate-upload', async (req, res) => {
  const { cid } = req.body;
  
  try {
    // Fetch and validate the uploaded content
    const response = await fetch(`https://ipfs.ninja/ipfs/${cid}`);
    const contentType = response.headers.get('content-type');
    
    // Implement your validation logic
    if (!isValidContentType(contentType)) {
      // Remove invalid content
      await deleteFromIPFS(cid);
      return res.status(400).json({ error: 'Invalid content type' });
    }
    
    res.json({ success: true, validated: true });
  } catch (error) {
    res.status(500).json({ error: 'Validation failed' });
  }
});

Обмеження частоти запитів

Реалізуйте додаткове обмеження частоти запитів на ендпоінт генерації токенів:

const rateLimit = require('express-rate-limit');

const tokenLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10, // Limit each IP to 10 token requests per windowMs
  message: 'Too many token requests, please try again later'
});

app.use('/api/generate-upload-token', tokenLimiter);

Переваги порівняно з традиційними підходами

Підписані токени завантаження забезпечують низку переваг порівняно з альтернативними методами безпеки завантаження в IPFS:

Порівняно з проксуванням на стороні сервера

  • Продуктивність: прямі завантаження виключають використання серверної пропускної здатності
  • Масштабованість: відсутність серверних вузьких місць при високому навантаженні завантажень
  • Вартість: зниження витрат на пропускну здатність та обробку
  • Досвід користувача: вища швидкість завантаження та відстеження прогресу

Порівняно з API-ключами на стороні клієнта

  • Безпека: немає ризику витягнення або зловживання API-ключем
  • Відповідність вимогам: задовольняє вимоги аудиту безпеки
  • Контроль доступу: детальні дозволи та автоматичне закінчення терміну дії
  • Моніторинг: покращене відстеження джерел та патернів завантажень

Порівняно з іншими сервісами закріплення

IPFS.NINJA наразі є єдиним великим сервісом закріплення, що пропонує підписані токени завантаження. Конкуренти, такі як Pinata, вимагають або проксування на стороні сервера, або розкриття API-ключа на стороні клієнта, що робить цю функцію унікальною перевагою.

Детальніше про те, як IPFS.NINJA порівнюється з іншими сервісами, читайте в нашому докладному посібнику з порівняння.

Поради щодо розгортання у продакшені

При розгортанні підписаних токенів завантаження у продакшені:

Конфігурація середовища

Зберігайте конфіденційну конфігурацію безпечно:

// Use environment variables for production
const config = {
  ipfsApiKey: process.env.IPFS_API_KEY,
  tokenExpiry: process.env.UPLOAD_TOKEN_EXPIRY || '1h',
  maxFileSize: process.env.MAX_FILE_SIZE_MB || 50,
  allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || ['localhost:3000']
};

Моніторинг та логування

Реалізуйте комплексне логування для моніторингу безпеки:

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'upload-security.log' })
  ]
});

// Log token generation
logger.info('Upload token generated', {
  userId: req.user.id,
  clientIP: req.ip,
  userAgent: req.get('User-Agent'),
  expiresAt: tokenData.expiresAt
});

Обробка помилок

Реалізуйте надійну обробку помилок, яка не розкриває конфіденційну інформацію:

app.use((error, req, res, next) => {
  // Log full error details server-side
  logger.error('Upload token error', {
    error: error.message,
    stack: error.stack,
    userId: req.user?.id,
    endpoint: req.path
  });
  
  // Send safe error message to client
  res.status(500).json({
    success: false,
    error: 'An internal error occurred. Please try again.'
  });
});

Інтеграція з популярними фреймворками

Підписані токени завантаження легко інтегруються з сучасними веб-фреймворками. Ось короткі приклади інтеграції:

React Hook

import { useState, useCallback } from 'react';

export function useSecureIPFSUpload() {
  const [uploading, setUploading] = useState(false);
  const [uploadToken, setUploadToken] = useState(null);

  const getToken = useCallback(async () => {
    if (uploadToken?.expiresAt && new Date() < new Date(uploadToken.expiresAt)) {
      return uploadToken.token;
    }

    const response = await fetch('/api/generate-upload-token', {
      method: 'POST'
    });
    const data = await response.json();
    setUploadToken(data);
    return data.uploadToken;
  }, [uploadToken]);

  const uploadFile = useCallback(async (file) => {
    setUploading(true);
    try {
      const token = await getToken();
      // Upload logic here...
    } finally {
      setUploading(false);
    }
  }, [getToken]);

  return { uploadFile, uploading };
}

Vue.js Composable

import { ref } from 'vue';

export function useSecureUpload() {
  const uploading = ref(false);
  const uploadProgress = ref(0);

  const uploadFile = async (file) => {
    uploading.value = true;
    // Implementation here...
  };

  return {
    uploading: readonly(uploading),
    uploadProgress: readonly(uploadProgress),
    uploadFile
  };
}

Висновок

Підписані токени завантаження вирішують критично важливу проблему безпеки в розробці децентралізованих додатків. Надаючи безпечний спосіб прямого клієнтського завантаження в IPFS без розкриття API-ключів, вони відкривають нові архітектурні можливості для сучасних веб-додатків.

Незалежно від того, чи створюєте ви систему керування контентом, маркетплейс NFT або будь-який додаток, що потребує безпечного завантаження файлів, токени завантаження IPFS.NINJA забезпечують необхідну безпеку та гнучкість. Реалізація проста, переваги безпеки значні, а приріст продуктивності суттєвий.

Щоб дізнатися більше про основи IPFS, ознайомтеся з нашим посібником що таке закріплення IPFS або вивчіть наш повний підручник з API. Для розробників, що оцінюють різні варіанти, наше порівняння найкращих сервісів закріплення IPFS надасть вичерпну інформацію.

Готові реалізувати безпечне клієнтське завантаження в IPFS у вашому додатку? Поєднання сучасних практик безпеки та децентралізованого сховища робить цей підхід ідеальним для продакшен-додатків, яким потрібно масштабуватися, зберігаючи стандарти безпеки.

Готові почати закріплення? Створіть безкоштовний акаунт — 500 файлів, 1 ГБ сховища, виділений шлюз. Кредитна картка не потрібна.

Назад до Блогу

Схожі статті

Усі статті »