· Nacho Coll · Guides  · 7 мин читања

IPFS токени за отпремање: безбедно отпремање са клијентске стране без излагања API кључева

Научите како потписани токени за отпремање омогућавају безбедно отпремање у IPFS из прегледача и мобилних апликација без излагања вашег API кључа.

Научите како потписани токени за отпремање омогућавају безбедно отпремање у IPFS из прегледача и мобилних апликација без излагања вашег API кључа.

Развој модерних веб апликација често захтева отпремање фајлова директно из прегледача корисника у облак складиштење. Међутим, када је реч о безбедности отпремања у IPFS, програмери се суочавају са тешком дилемом: како дозволити отпремање са клијентске стране без излагања драгоцених API кључева потенцијалној злоупотреби?

Већина IPFS сервиса за закачивање вас ставља пред непријатан избор: или обрадити сва отпремања на серверској страни (стварајући уска грла и сложеност) или уградити API кључ у клијентски код (безбедносна ноћна мора). IPFS.NINJA решава ово јединственом функцијом коју ниједан други сервис за закачивање не нуди: потписани токени за отпремање.

IPFS Ninja

Проблем са традиционалним IPFS API кључевима

Ризик од излагања API кључа

Уградња API кључева директно у JavaScript прегледача значи да свако може прегледати ваш изворни код и извући акредитиве. То може довести до:

  • Неовлашћених отпремања која троше вашу квоту складиштења
  • Потенцијалне злоупотребе вашег налога сервиса за закачивање
  • Кршења безбедносних захтева у корпоративним окружењима

Уска грла на серверу

  • Повећани трошкови серверског пропусног опсега
  • Веће кашњење за кориснике
  • Сложенији инфраструктурни захтеви
  • Потенцијалне јединствене тачке отказа

Безбедност мобилних апликација

Мобилне апликације се суочавају са сличним изазовима: API кључеви ускладиштени у пакетима апликација могу бити извучени реверзним инжењерингом.

Представљамо IPFS токене за отпремање

Потписани токени за отпремање IPFS.NINJA пружају безбедан компромис:

  1. Сервер генерише токен: ваш бекенд креира временски ограничен, потписан токен користећи ваш API кључ
  2. Клијент прима токен: токен се безбедно преноси вашој фронтенд апликацији
  3. Директно отпремање: клијенти отпремају директно у IPFS.NINJA користећи потписан токен
  4. Аутоматско истицање: токени истичу након задатог периода, ограничавајући прозор изложености

Разумевање безбедности токена за отпремање

Сваки токен садржи:

  • Временска ознака истицања: аутоматско поништавање након одређеног периода
  • Ограничења употребе: опциони лимити на број фајлова или укупну величину
  • Криптографски потпис: спречава фалсификовање
  • Верификација издавача: везано за ваш аутентификовани налог

Имплементација бекенда: Express.js пример

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

Имплементација фронтенда: безбедно клијентско отпремање

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

Напредна безбедносна разматрања

Ограничења опсега токена

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

Валидација садржаја

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

Ограничавање брзине

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

Предности у односу на традиционалне приступе

Наспрам серверског проксирања

  • Перформансе: директна отпремања елиминишу коришћење серверског пропусног опсега
  • Скалабилност: без серверских уских грла
  • Трошкови: смањени трошкови за пропусни опсег

Наспрам API кључева на клијентској страни

  • Безбедност: нема ризика од излагања API кључа
  • Усклађеност: задовољава захтеве безбедносног аудита
  • Контрола приступа: детаљне дозволе и аутоматско истицање

Наспрам других сервиса за закачивање

IPFS.NINJA је тренутно једини велики сервис за закачивање који нуди потписане токене за отпремање. За више информација погледајте наш водич за поређење.

Савети за продукцијско постављање

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

Интеграција са популарним фрејмворцима

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

Закључак

Потписани токени за отпремање решавају критичан безбедносни изазов у развоју децентрализованих апликација. Научите више у нашем водичу шта је IPFS закачивање или истражите наш комплетан API водич. За поређење видите најбоље IPFS сервисе за закачивање.

Спремни за закачивање? Направите бесплатан налог — 500 фајлова, 1 GB складиштења, наменски гејтвеј. Кредитна картица није потребна.

Назад на Блог

Повезани чланци

Погледајте све чланке »