· Nacho Coll · Guides  · 10 menit baca

Token Upload IPFS: Upload Sisi Klien yang Aman Tanpa Mengekspos API Key

Pelajari bagaimana signed upload token memungkinkan Anda mengunggah ke IPFS dengan aman dari browser dan aplikasi mobile tanpa mengekspos API key Anda.

Pelajari bagaimana signed upload token memungkinkan Anda mengunggah ke IPFS dengan aman dari browser dan aplikasi mobile tanpa mengekspos API key Anda.

Membangun aplikasi web modern sering kali memerlukan pengunggahan file langsung dari browser pengguna ke penyimpanan cloud. Namun, dalam hal keamanan upload IPFS, pengembang menghadapi dilema yang menantang: bagaimana cara mengizinkan upload sisi klien tanpa mengekspos API key Anda yang berharga terhadap potensi penyalahgunaan?

Sebagian besar layanan pinning IPFS memaksa Anda memilih pilihan yang tidak nyaman: menangani semua upload di sisi server (menciptakan bottleneck dan kompleksitas) atau menyematkan API key Anda di kode klien (mimpi buruk keamanan). IPFS.NINJA menyelesaikan masalah ini dengan fitur unik yang tidak ditawarkan layanan pinning lain: signed upload token.

IPFS Ninja

Masalah dengan API Key IPFS Tradisional

Ketika membangun aplikasi sisi klien yang perlu mengunggah ke IPFS, pengembang biasanya menghadapi beberapa tantangan keamanan:

Risiko Tereksposnya API Key

Menyematkan API key langsung di JavaScript browser berarti siapa pun bisa melihat kode sumber Anda dan mengekstrak kredensial Anda. Ini bisa menyebabkan:

  • Upload tidak sah yang menghabiskan kuota penyimpanan Anda
  • Potensi penyalahgunaan akun layanan pinning Anda
  • Pelanggaran kepatuhan keamanan di lingkungan enterprise

Bottleneck Sisi Server

Alternatifnya—mengalirkan semua upload melalui backend Anda—menciptakan beberapa masalah:

  • Peningkatan biaya bandwidth server
  • Latensi lebih tinggi bagi pengguna
  • Persyaratan infrastruktur yang lebih kompleks
  • Potensi titik kegagalan tunggal

Keamanan Aplikasi Mobile

Aplikasi mobile menghadapi tantangan serupa, di mana API key yang disimpan dalam bundle aplikasi dapat diekstrak melalui reverse engineering.

Memperkenalkan Token Upload IPFS

Signed upload token dari IPFS.NINJA menyediakan jalan tengah yang aman. Berikut cara kerjanya:

  1. Server menghasilkan token: Backend Anda membuat token bertanda tangan dengan batas waktu menggunakan API key Anda
  2. Klien menerima token: Token ditransmisikan dengan aman ke aplikasi frontend Anda
  3. Upload langsung: Klien mengunggah langsung ke IPFS.NINJA menggunakan signed token
  4. Kedaluwarsa otomatis: Token kedaluwarsa setelah durasi yang ditentukan, membatasi jendela paparan

Pendekatan ini menggabungkan keamanan autentikasi sisi server dengan manfaat kinerja upload langsung dari klien.

Memahami Keamanan Upload Token

Signed upload token menggunakan tanda tangan kriptografi untuk memastikan keaslian tanpa mengekspos API key utama Anda. Setiap token berisi:

  • Timestamp kedaluwarsa: Invalidasi otomatis setelah durasi yang ditentukan
  • Batasan penggunaan: Batas opsional pada jumlah file atau ukuran total
  • Tanda tangan kriptografi: Mencegah pemalsuan atau peniruan
  • Verifikasi penerbit: Terhubung kembali ke akun Anda yang terautentikasi

Tidak seperti API key, upload token dirancang agar aman disematkan di kode sisi klien. Bahkan jika diekstrak, token tersebut menyediakan akses terbatas yang kedaluwarsa secara otomatis.

Implementasi Backend: Contoh Express.js

Mari kita buat contoh lengkap yang menunjukkan cara mengimplementasikan upload IPFS sisi klien yang aman. Pertama, berikut backend Express.js yang menghasilkan upload token:

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

Implementasi Frontend: Upload Klien yang Aman

Sekarang, berikut kode frontend yang mengunggah file dengan aman menggunakan signed token:

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

Pertimbangan Keamanan Lanjutan

Ketika mengimplementasikan keamanan upload IPFS dengan signed token, pertimbangkan langkah-langkah keamanan tambahan berikut:

Pembatasan Cakupan Token

Konfigurasikan token dengan batasan yang sesuai:

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

Validasi Konten

Selalu validasi konten yang diunggah di backend Anda:

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

Pembatasan Laju (Rate Limiting)

Terapkan pembatasan laju tambahan pada endpoint pembuatan token Anda:

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

Keunggulan Dibanding Pendekatan Tradisional

Signed upload token memberikan beberapa keunggulan dibandingkan metode keamanan upload IPFS alternatif:

vs. Proxy Sisi Server

  • Kinerja: Upload langsung menghilangkan penggunaan bandwidth server
  • Skalabilitas: Tidak ada bottleneck server selama periode upload tinggi
  • Biaya: Pengurangan biaya bandwidth dan pemrosesan
  • Pengalaman Pengguna: Kecepatan upload dan pelacakan progres yang lebih baik

vs. API Key Sisi Klien

  • Keamanan: Tidak ada risiko ekstraksi atau penyalahgunaan API key
  • Kepatuhan: Memenuhi persyaratan audit keamanan
  • Kontrol Akses: Izin yang detail dan kedaluwarsa otomatis
  • Pemantauan: Pelacakan sumber dan pola upload yang lebih baik

vs. Layanan Pinning Lain

IPFS.NINJA saat ini merupakan satu-satunya layanan pinning utama yang menawarkan signed upload token. Pesaing seperti Pinata memerlukan proxy sisi server atau eksposur API key sisi klien, menjadikan ini pembeda yang unik.

Untuk detail lebih lanjut tentang perbandingan IPFS.NINJA dengan layanan lain, lihat panduan perbandingan komprehensif kami.

Tips Deployment Produksi

Saat men-deploy signed upload token di produksi:

Konfigurasi Lingkungan

Simpan konfigurasi sensitif dengan aman:

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

Pemantauan dan Pencatatan Log

Terapkan pencatatan log yang komprehensif untuk pemantauan keamanan:

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

Penanganan Error

Terapkan penanganan error yang kokoh dan tidak membocorkan informasi sensitif:

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

Integrasi dengan Framework Populer

Signed upload token bekerja dengan mulus di framework web modern. Berikut contoh integrasi cepat:

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

Kesimpulan

Signed upload token memecahkan tantangan keamanan kritis dalam pengembangan aplikasi terdesentralisasi. Dengan menyediakan cara aman untuk mengaktifkan upload langsung sisi klien ke IPFS tanpa mengekspos API key, mereka membuka kemungkinan arsitektur baru untuk aplikasi web modern.

Baik Anda membangun sistem manajemen konten, marketplace NFT, atau aplikasi apa pun yang memerlukan upload file yang aman, upload token IPFS.NINJA menyediakan keamanan dan fleksibilitas yang Anda butuhkan. Implementasinya sederhana, manfaat keamanannya signifikan, dan peningkatan kinerjanya substansial.

Untuk mempelajari lebih lanjut tentang dasar-dasar IPFS, lihat panduan kami tentang apa itu IPFS pinning atau jelajahi tutorial API lengkap kami. Untuk pengembang yang mengevaluasi berbagai opsi, perbandingan layanan pinning IPFS terbaik kami menyediakan wawasan komprehensif.

Siap mengimplementasikan upload IPFS sisi klien yang aman di aplikasi Anda? Kombinasi praktik keamanan modern dan penyimpanan terdesentralisasi menjadikan pendekatan ini ideal untuk aplikasi produksi yang perlu berkembang sambil menjaga standar keamanan.

Siap mulai pinning? Buat akun gratis — 500 file, penyimpanan 1 GB, gateway khusus. Tidak perlu kartu kredit.

Kembali ke Blog

Artikel Terkait

Lihat Semua Artikel »