· 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 ГБ хранилища, выделенный шлюз. Кредитная карта не требуется.

Назад в Блог

Похожие статьи

Все статьи »