Tokens de Pujada IPFS: Pujades Segures del Client

Aprèn com els tokens d'uploading signats et permeten pujar fitxers a IPFS des de navegadors i apps mòbils sense exposar la teva clau API.

Nacho Collper Actualitzat: 13 min de lectura
Aprèn com els tokens d'uploading signats et permeten pujar fitxers a IPFS des de navegadors i apps mòbils sense exposar la teva clau API.
TL;DR
  • Genera tokens de pujada signats des del servidor perquè els navegadors i les apps mòbils mai vegin la teva clau API en cru.
  • Els tokens porten una marca de temps d'expiració i restriccions d'ús, limitant els danys si algun s'arriba a extreure.
  • Les pujades directes del client a IPFS mitjançant tokens eliminen completament els colls d'ampolla d'ample de banda del servidor.
  • Les signatures criptogràfiques de cada token impedeixen manipular-lo o falsificar una credencial de pujada.

Construir aplicacions web modernes sovint requereix pujar fitxers directament des dels navegadors dels usuaris a l’emmagatzematge al núvol. No obstant això, quan es tracta de la seguretat de pujada IPFS, els desenvolupadors s’enfronten a un dilema desafiant: com permetre pujades del costat del client sense exposar les vostres valuoses claus API a un possible ús indegut?

La majoria de serveis de pinning IPFS us obliguen a fer una elecció incòmoda: gestionar totes les pujades al costat del servidor (creant colls d’ampolla i complexitat) o incrustar la vostra clau API al codi del client (un malson de seguretat). IPFS.NINJA resol això amb una característica única que cap altre servei de pinning ofereix: tokens de pujada signats.

IPFS Ninja

El Problema amb les Claus API IPFS Tradicionals#

Quan es construeixen aplicacions del costat del client que necessiten pujar a IPFS, els desenvolupadors típicament troben diversos reptes de seguretat:

Risc d’Exposició de la Clau API#

Incrustar claus API directament al JavaScript del navegador significa que qualsevol pot veure el vostre codi font i extreure les vostres credencials. Això podria provocar:

  • Pujades no autoritzades que consumeixen la vostra quota d’emmagatzematge
  • Possible abús del vostre compte de servei de pinning
  • Violacions de compliment de seguretat en entorns empresarials

Colls d’Ampolla del Costat del Servidor#

L’alternativa---encaminar totes les pujades a través del vostre backend---crea diversos problemes:

  • Augment dels costos d’ample de banda del servidor
  • Major latència per als usuaris
  • Requisits d’infraestructura més complexos
  • Possibles punts únics de fallada

Seguretat d’Aplicacions Mòbils#

Les aplicacions mòbils s’enfronten a reptes similars, on les claus API emmagatzemades als paquets d’aplicacions es poden extreure mitjançant enginyeria inversa.

Presentant els Tokens de Pujada IPFS#

Els tokens de pujada signats d’IPFS.NINJA proporcionen un punt mig segur. Així és com funcionen:

  1. El servidor genera el token: El vostre backend crea un token signat amb límit de temps utilitzant la vostra clau API
  2. El client rep el token: El token es transmet de forma segura a la vostra aplicació frontend
  3. Pujada directa: Els clients pugen directament a IPFS.NINJA utilitzant el token signat
  4. Caducitat automàtica: Els tokens caduquen després d’una durada establerta, limitant la finestra d’exposició

Aquesta aproximació combina la seguretat de l’autenticació del costat del servidor amb els beneficis de rendiment de les pujades directes del client.

Entenent la Seguretat dels Upload Tokens#

Els tokens de pujada signats utilitzen signatures criptogràfiques per assegurar l’autenticitat sense exposar la vostra clau API principal. Cada token conté:

  • Marca de temps de caducitat: Invalidació automàtica després de la durada especificada
  • Restriccions d’ús: Límits opcionals en el nombre de fitxers o la mida total
  • Signatura criptogràfica: Impedeix la manipulació o la falsificació
  • Verificació de l’emissor: Enllaça de tornada al vostre compte autenticat

A diferència de les claus API, els tokens de pujada estan dissenyats per ser incrustats de forma segura al codi del costat del client. Fins i tot si s’extreuen, proporcionen un accés limitat que caduca automàticament.

Implementació del Backend: Exemple amb Express.js#

Construïm un exemple complet que mostra com implementar pujades IPFS segures del costat del client. Primer, aquí teniu el backend Express.js que genera tokens de pujada:

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

Implementació del Frontend: Pujada Segura del Client#

Ara, aquí teniu el codi del frontend que puja fitxers de forma segura utilitzant el token signat:

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

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

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

            async getUploadToken() {
                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();
                    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 {
                    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 = () => { resolve(reader.result.split(',')[1]); };
                    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 = ''; }
        }

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

Consideracions de Seguretat Avançades#

Quan implementeu seguretat de pujada IPFS amb tokens signats, considereu aquestes mesures de seguretat addicionals:

Limitacions d’Abast del Token#

Configureu els tokens amb restriccions adequades:

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

Validació de Contingut#

Valideu sempre el contingut pujat al vostre backend:

app.post('/api/validate-upload', async (req, res) => {
  const { cid } = req.body;
  
  try {
    const response = await fetch(`https://ipfs.ninja/ipfs/${cid}`);
    const contentType = response.headers.get('content-type');
    
    if (!isValidContentType(contentType)) {
      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' });
  }
});

Limitació de Velocitat#

Implementeu limitació de velocitat addicional al vostre endpoint de generació de tokens:

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

Avantatges Respecte als Enfocaments Tradicionals#

Els tokens de pujada signats proporcionen diversos avantatges respecte als mètodes alternatius de seguretat de pujada IPFS:

vs. Proxy del Costat del Servidor#

  • Rendiment: Les pujades directes eliminen l’ús d’ample de banda del servidor
  • Escalabilitat: Sense colls d’ampolla del servidor durant períodes d’alta pujada
  • Cost: Reducció de costos d’ample de banda i processament
  • Experiència d’Usuari: Millor velocitat de pujada i seguiment del progrés

vs. Clau API del Costat del Client#

  • Seguretat: Sense risc d’extracció o ús indegut de la clau API
  • Compliment: Compleix els requisits d’auditoria de seguretat
  • Control d’Accés: Permisos detallats i caducitat automàtica
  • Monitoratge: Millor seguiment de les fonts i patrons de pujada

vs. Altres Serveis de Pinning#

IPFS.NINJA és actualment l’únic servei de pinning important que ofereix tokens de pujada signats. Competidors com Pinata requereixen proxy del costat del servidor o exposició de la clau API del costat del client, fent d’això un diferenciador únic.

Per a més detalls sobre com IPFS.NINJA es compara amb altres serveis, consulteu la nostra guia de comparació completa.

Consells per al Desplegament en Producció#

Quan desplegeu tokens de pujada signats en producció:

Configuració de l’Entorn#

Emmagatzemeu la configuració sensible de forma segura:

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

Monitoratge i Registre#

Implementeu un registre complet per al monitoratge de seguretat:

const winston = require('winston');

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

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

Gestió d’Errors#

Implementeu una gestió d’errors robusta que no filtri informació sensible:

app.use((error, req, res, next) => {
  logger.error('Upload token error', {
    error: error.message,
    stack: error.stack,
    userId: req.user?.id,
    endpoint: req.path
  });
  
  res.status(500).json({
    success: false,
    error: 'An internal error occurred. Please try again.'
  });
});

Integració amb Frameworks Populars#

Els tokens de pujada signats funcionen perfectament amb els frameworks web moderns. Aquí teniu exemples ràpids d’integració:

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

Conclusió#

Els tokens de pujada signats resolen un repte de seguretat crític en el desenvolupament d’aplicacions descentralitzades. En proporcionar una manera segura de permetre pujades directes del costat del client a IPFS sense exposar les claus API, obren noves possibilitats arquitectòniques per a les aplicacions web modernes.

Tant si esteu construint un sistema de gestió de continguts, un marketplace NFT o qualsevol aplicació que requereixi pujades de fitxers segures, els tokens de pujada d’IPFS.NINJA proporcionen la seguretat i flexibilitat que necessiteu. La implementació és senzilla, els beneficis de seguretat són significatius i els guanys de rendiment són substancials.

Per aprendre més sobre els fonaments d’IPFS, consulteu la nostra guia sobre què és l’IPFS pinning o exploreu el nostre tutorial complet de l’API. Per als desenvolupadors que avaluen diferents opcions, la nostra comparació dels millors serveis de pinning IPFS proporciona informació completa.

Esteu a punt per implementar pujades IPFS segures del costat del client a la vostra aplicació? La combinació de pràctiques de seguretat modernes i emmagatzematge descentralitzat fa que aquesta aproximació sigui ideal per a aplicacions de producció que necessiten escalar mantenint els estàndards de seguretat.

Esteu a punt per començar el pinning? Creeu un compte gratuït --- 500 fitxers, 1 GB d’emmagatzematge, gateway dedicat. No cal targeta de crèdit.

Nacho Coll

Sobre l'autor

Founder & Engineer at IPFS.NINJA

Nacho founded IPFS.NINJA to make content-addressed storage feel as simple as an S3 PUT — a single API call, a permanent CID, no wallets or peer discovery to reason about. Writes about IPFS internals, decentralized storage patterns, and the pinning-service landscape from the operator side of the wire.

Tornar al Blog

Articles Relacionats