IPFS žetoni za nalaganje: varno nalaganje na odjemalcu

Spoznajte, kako podpisani žetoni za nalaganje omogočajo varno nalaganje v IPFS iz brskalnikov in mobilnih aplikacij brez razkrivanja vašega API ključa.

Nacho Collavtor Posodobljeno: 10 min branja
Spoznajte, kako podpisani žetoni za nalaganje omogočajo varno nalaganje v IPFS iz brskalnikov in mobilnih aplikacij brez razkrivanja vašega API ključa.
TL;DR
  • Ustvarite podpisane žetone za nalaganje na strani strežnika, da brskalniki in mobilne aplikacije nikoli ne vidijo vašega surovega API ključa.
  • Žetoni vsebujejo časovni žig poteka veljavnosti in omejitve uporabe, kar omeji škodo, če bi bil kdaj pridobljen.
  • Neposredna nalaganja od odjemalca do IPFS prek žetonov popolnoma odpravijo ozka grla pasovne širine strežnika.
  • Kriptografski podpisi na vsakem žetonu preprečujejo poseganje vanj ali ponarejanje lažnega poverila za nalaganje.

Razvoj sodobnih spletnih aplikacij pogosto zahteva nalaganje datotek neposredno iz brskalnikov uporabnikov v oblačno shrambo. Ko pa gre za varnost nalaganja v IPFS, se razvijalci soočajo z zahtevno dilemo: kako omogočiti nalaganje na strani odjemalca brez razkrivanja dragocenih API ključev potencialni zlorabi?

Večina IPFS storitev za pripenjanje vas postavlja pred neprijetno izbiro: bodisi obravnavate vsa nalaganja na strani strežnika (ustvarjate ozka grla in kompleksnost) ali pa vgradite API ključ v odjemalčevo kodo (varnostna nočna mora). IPFS.NINJA to rešuje z edinstveno funkcijo, ki je nobena druga storitev za pripenjanje ne ponuja: podpisani žetoni za nalaganje.

IPFS Ninja

Problem s tradicionalnimi IPFS API ključi#

Tveganje razkritja API ključa#

Vgradnja API ključev neposredno v JavaScript brskalnika pomeni, da lahko kdorkoli pregleda vašo izvorno kodo in izvleče poverilnice. To lahko vodi do:

  • Nepooblaščenih nalaganj, ki porabljajo vašo kvoto shrambe
  • Potencialne zlorabe vašega računa storitve za pripenjanje
  • Kršitev varnostnih zahtev v podjetniških okoljih

Ozka grla na strani strežnika#

  • Povečani stroški pasovne širine strežnika
  • Večja zakasnitev za uporabnike
  • Zahtevnejša infrastruktura
  • Potencialne posamezne točke odpovedi

Varnost mobilnih aplikacij#

API ključi, shranjeni v paketih aplikacij, se lahko izvlečejo z obratnim inženirstvom.

Predstavljamo IPFS žetone za nalaganje#

Podpisani žetoni za nalaganje IPFS.NINJA zagotavljajo varen kompromis:

  1. Strežnik generira žeton: vaš zaledni del ustvari časovno omejen, podpisan žeton z uporabo vašega API ključa
  2. Odjemalec prejme žeton: žeton se varno prenese v vašo sprednjo aplikacijo
  3. Neposredno nalaganje: odjemalci nalagajo neposredno v IPFS.NINJA z uporabo podpisanega žetona
  4. Samodejni potek: žetoni potečejo po nastavljenem obdobju

Razumevanje varnosti žetonov za nalaganje#

Vsak žeton vsebuje:

  • Časovni žig poteka: samodejna razveljavitev po določenem obdobju
  • Omejitve uporabe: neobvezne omejitve na število datotek ali skupno velikost
  • Kriptografski podpis: preprečuje poseganje ali ponarejanje
  • Preverjanje izdajatelja: povezava z vašim overjenem računom

Implementacija zalednega dela: Express.js primer#

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

const IPFS_API_KEY = 'bws_1234567890abcdef1234567890abcdef12345678';

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', maxUploads: 10, maxSizeMB: 50 })
    });
    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' });
  }
});

app.post('/api/verify-upload', async (req, res) => {
  const { cid } = req.body;
  try {
    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'); });

Implementacija sprednjega dela: Varno nalaganje odjemalca#

<!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 results = await Promise.all(files.map(file => this.uploadFile(file)));
                    this.displayResults(results);
                    const successful = results.filter(r => r.success).length;
                    if (successful === results.length) this.showStatus(`✅ Successfully uploaded ${successful} file(s) to IPFS!`, 'success');
                    else this.showStatus(`⚠️ Uploaded ${successful}/${results.length} 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 s = document.getElementById('status'); s.className = `status ${type}`; s.textContent = message; }
            displayResults(results) { document.getElementById('results').innerHTML = '<h3>Upload Results:</h3>' + results.map(r => `<div class="status ${r.success ? 'success' : 'error'}" style="margin:10px 0;"><strong>${r.filename}</strong><br>${r.success ? `✅ CID: ${r.cid}<br>📊 Size: ${r.size} MB<br>🔗 URL: <a href="${r.httpUrl}" target="_blank">${r.httpUrl}</a>` : `❌ Error: ${r.error}`}</div>`).join(''); }
            clearResults() { document.getElementById('results').innerHTML = ''; }
        }
        const uploader = new SecureIPFSUploader();
    </script>
</body>
</html>

Napredni varnostni vidiki#

Omejitve obsega žetona#

const restrictedToken = await fetch('https://api.ipfs.ninja/upload-tokens', {
  method: 'POST',
  headers: { 'X-Api-Key': IPFS_API_KEY },
  body: JSON.stringify({ expiresIn: '30m', maxUploads: 5, maxSizeMB: 10, allowedMimeTypes: ['image/jpeg', 'image/png'], ipWhitelist: ['192.168.1.0/24'] })
});

Validacija vsebine#

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

Omejevanje hitrosti#

const rateLimit = require('express-rate-limit');
const tokenLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10, message: 'Too many token requests, please try again later' });
app.use('/api/generate-upload-token', tokenLimiter);

Prednosti pred tradicionalnimi pristopi#

V primerjavi s strežniškim posredovanjem#

  • Zmogljivost: neposredna nalaganja odpravijo uporabo pasovne širine strežnika
  • Razširljivost: brez strežniških ozkih grl
  • Stroški: zmanjšani stroški za pasovno širino

V primerjavi z API ključi na strani odjemalca#

  • Varnost: brez tveganja izvlečenja API ključa
  • Skladnost: izpolnjuje zahteve varnostnega pregleda
  • Nadzor dostopa: natančna dovoljenja in samodejni potek

V primerjavi z drugimi storitvami za pripenjanje#

IPFS.NINJA je edina velika storitev za pripenjanje, ki ponuja podpisane žetone. Več v našem vodniku za primerjavo.

Nasveti za uvajanje v produkcijo#

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

Integracija s priljubljenimi ogrodji#

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(); } 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; };
  return { uploading: readonly(uploading), uploadProgress: readonly(uploadProgress), uploadFile };
}

Konfiguracija obsega žetona#

Pri ustvarjanju žetona nastavite expiresIn, maxUploads, maxSizeMB, allowedMimeTypes in ipWhitelist, da omejite, kaj lahko odjemalec s tem žetonom naredi:

const token = await fetch("https://api.ipfs.ninja/upload-tokens", {
  method: "POST",
  headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    expiresIn: "30m",
    maxUploads: 5,
    maxSizeMB: 20,
    allowedMimeTypes: ["image/jpeg", "image/png"],
    ipWhitelist: ["203.0.113.0/24"],
  }),
});

Vsaka od teh omejitev se uveljavi na strežniku ob vsakem nalaganju, zato je napadalec — tudi če pridobi žeton iz odjemalske kode — omejen natanko na te meje, dokler žeton ne poteče.

Integracija z mobilnimi aplikacijami#

Mobilne aplikacije (iOS, Android, React Native) zahtevajo žeton neposredno od vašega lastnega zalednega strežnika — nikoli neposredno od IPFS API-ja — in ga uporabijo za neposredno nalaganje. Zaledje obdrži API ključ, mobilna aplikacija pa prejme le kratkotrajen podpisan žeton. Tako lahko aplikacija nalaga datoteke neposredno, brez preusmerjanja prek vašega strežnika, kar zmanjša porabo pasovne širine in zakasnitev, API ključ pa nikoli ne zapusti zaledja.

Zaključek#

Podpisani žetoni za nalaganje rešujejo kritičen varnostni problem pri razvoju decentraliziranih aplikacij. Več se naučite v našem vodniku kaj je IPFS pripenjanje ali raziščite naš popoln API vodnik. Za primerjavo si oglejte najboljše storitve za pripenjanje IPFS.

Pripravljeni na pripenjanje? Ustvarite brezplačen račun — 500 datotek, 1 GB shrambe, namenski prehod. Kreditna kartica ni potrebna.

Nacho Coll

O avtorju

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.

Nazaj na Blog

Povezani članki