· Nacho Coll · Guides  · 10 min čtení

Tokeny pro nahrávání na IPFS: bezpečné nahrávání na straně klienta bez odhalení API klíčů

Zjistěte, jak podepsané tokeny pro nahrávání umožňují bezpečně nahrávat soubory na IPFS z prohlížečů a mobilních aplikací bez odhalení API klíče.

Zjistěte, jak podepsané tokeny pro nahrávání umožňují bezpečně nahrávat soubory na IPFS z prohlížečů a mobilních aplikací bez odhalení API klíče.

Vývoj moderních webových aplikací často vyžaduje nahrávání souborů přímo z prohlížečů uživatelů do cloudového úložiště. Pokud jde však o bezpečnost nahrávání na IPFS, vývojáři čelí náročnému dilematu: jak umožnit nahrávání na straně klienta bez odhalení cenných API klíčů potenciálnímu zneužití?

Většina IPFS pinning služeb vás nutí do nepohodlné volby: buď zpracujete všechna nahrávání na straně serveru (vytváříte úzká hrdla a složitost), nebo vložíte API klíč do kódu klienta (bezpečnostní noční můra). IPFS.NINJA to řeší jedinečnou funkcí, kterou žádná jiná pinning služba nenabízí: podepsané tokeny pro nahrávání.

IPFS Ninja

Problém s tradičními IPFS API klíči

Při vývoji aplikací na straně klienta, které potřebují nahrávat na IPFS, vývojáři obvykle narazí na několik bezpečnostních výzev:

Riziko odhalení API klíče

Vložení API klíčů přímo do JavaScriptu prohlížeče znamená, že kdokoli může zobrazit váš zdrojový kód a extrahovat vaše přihlašovací údaje. To by mohlo vést k:

  • Neautorizovaným nahráváním spotřebovávajícím vaši kvótu úložiště
  • Potenciálnímu zneužití vašeho účtu pinning služby
  • Porušení bezpečnostních předpisů v podnikových prostředích

Úzká hrdla na straně serveru

Alternativa — směrování všech nahrávání přes váš backend — vytváří několik problémů:

  • Zvýšené náklady na šířku pásma serveru
  • Vyšší latence pro uživatele
  • Složitější požadavky na infrastrukturu
  • Potenciální jediné body selhání

Bezpečnost mobilních aplikací

Mobilní aplikace čelí podobným výzvám — API klíče uložené v balíčcích aplikací mohou být extrahovány reverzním inženýrstvím.

Představujeme tokeny pro nahrávání na IPFS

Podepsané tokeny pro nahrávání od IPFS.NINJA poskytují bezpečný kompromis. Takto fungují:

  1. Server generuje token: Váš backend vytvoří časově omezený, podepsaný token pomocí vašeho API klíče
  2. Klient obdrží token: Token je bezpečně přenesen do vaší frontendové aplikace
  3. Přímé nahrávání: Klienti nahrávají přímo do IPFS.NINJA pomocí podepsaného tokenu
  4. Automatické vypršení: Tokeny vyprší po stanovené době, čímž se omezí okno expozice

Tento přístup kombinuje bezpečnost autentizace na straně serveru s výkonnostními výhodami přímého nahrávání klientem.

Porozumění bezpečnosti tokenů pro nahrávání

Podepsané tokeny pro nahrávání používají kryptografické podpisy k zajištění autenticity bez odhalení vašeho hlavního API klíče. Každý token obsahuje:

  • Časové razítko vypršení: Automatická invalidace po určené době
  • Omezení použití: Volitelné limity počtu souborů nebo celkové velikosti
  • Kryptografický podpis: Zabraňuje manipulaci nebo padělání
  • Ověření vydavatele: Propojení s vaším autentizovaným účtem

Na rozdíl od API klíčů jsou tokeny pro nahrávání navrženy tak, aby mohly být bezpečně vloženy do kódu na straně klienta. I kdyby byly extrahovány, poskytují omezený přístup, který automaticky vyprší.

Implementace backendu: příklad Express.js

Pojďme vytvořit kompletní příklad ukazující, jak implementovat bezpečné nahrávání na IPFS na straně klienta. Nejprve backend Express.js, který generuje tokeny pro nahrávání:

// 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');
});

Implementace frontendu: bezpečné nahrávání na straně klienta

Nyní kód frontendu, který bezpečně nahrává soubory pomocí podepsaného tokenu:

<!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>

Pokročilé bezpečnostní aspekty

Při implementaci bezpečnosti nahrávání na IPFS s podepsanými tokeny zvažte tato další bezpečnostní opatření:

Omezení rozsahu tokenu

Konfigurujte tokeny s příslušnými omezeními:

// 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
  })
});

Validace obsahu

Vždy validujte nahraný obsah na vašem backendu:

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' });
  }
});

Omezení četnosti požadavků

Implementujte dodatečné omezení četnosti na vašem endpointu pro generování tokenů:

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);

Výhody oproti tradičním přístupům

Podepsané tokeny pro nahrávání poskytují několik výhod oproti alternativním metodám zabezpečení nahrávání na IPFS:

Oproti proxy na straně serveru

  • Výkon: Přímé nahrávání eliminuje spotřebu šířky pásma serveru
  • Škálovatelnost: Žádná úzká hrdla serveru během období intenzivního nahrávání
  • Náklady: Snížené náklady na šířku pásma a zpracování
  • Uživatelský zážitek: Rychlejší nahrávání a lepší sledování průběhu

Oproti API klíčům na straně klienta

  • Bezpečnost: Žádné riziko extrakce nebo zneužití API klíče
  • Soulad s předpisy: Splňuje požadavky bezpečnostních auditů
  • Řízení přístupu: Jemně nastavitelná oprávnění a automatické vypršení
  • Monitoring: Lepší sledování zdrojů a vzorců nahrávání

Oproti jiným pinning službám

IPFS.NINJA je v současné době jedinou hlavní pinning službou nabízející podepsané tokeny pro nahrávání. Konkurenti jako Pinata vyžadují buď proxy na straně serveru, nebo odhalení API klíče na straně klienta, což z této funkce činí unikátní rozlišovací prvek.

Více informací o porovnání IPFS.NINJA s ostatními službami najdete v našem komplexním srovnávacím průvodci.

Tipy pro produkční nasazení

Při nasazování podepsaných tokenů pro nahrávání v produkci:

Konfigurace prostředí

Ukládejte citlivou konfiguraci bezpečně:

// 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']
};

Monitoring a logování

Implementujte komplexní logování pro bezpečnostní monitoring:

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
});

Zpracování chyb

Implementujte robustní zpracování chyb, které neprozrazuje citlivé informace:

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.'
  });
});

Integrace s populárními frameworky

Podepsané tokeny pro nahrávání bezproblémově fungují s moderními webovými frameworky. Zde jsou rychlé příklady integrace:

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
  };
}

Závěr

Podepsané tokeny pro nahrávání řeší kritický bezpečnostní problém ve vývoji decentralizovaných aplikací. Poskytují bezpečný způsob, jak umožnit přímé nahrávání na straně klienta do IPFS bez odhalení API klíčů, čímž otevírají nové architektonické možnosti pro moderní webové aplikace.

Ať už vytváříte systém správy obsahu, NFT marketplace nebo jakoukoli aplikaci vyžadující bezpečné nahrávání souborů, tokeny pro nahrávání od IPFS.NINJA poskytují potřebnou bezpečnost a flexibilitu. Implementace je přímočará, bezpečnostní výhody jsou významné a výkonnostní zisky jsou podstatné.

Chcete-li se dozvědět více o základech IPFS, přečtěte si náš průvodce Co je IPFS pinning nebo prozkoumejte náš kompletní API tutoriál. Pro vývojáře hodnotící různé možnosti nabízí naše srovnání nejlepších IPFS pinning služeb komplexní přehled.

Jste připraveni implementovat bezpečné nahrávání na IPFS na straně klienta ve vaší aplikaci? Kombinace moderních bezpečnostních postupů a decentralizovaného úložiště činí tento přístup ideálním pro produkční aplikace, které potřebují škálovat při zachování bezpečnostních standardů.

Připraveni začít s pinningem? Vytvořte si bezplatný účet — 500 souborů, 1 GB úložiště, dedikovaná brána. Kreditní karta není vyžadována.

Zpět na Blog

Související články

Zobrazit všechny články »