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

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

Nacho Collаутор Ажурирано: 11 мин читања
Научите како потписани токени за отпремање омогућавају безбедно отпремање у IPFS из прегледача и мобилних апликација без излагања вашег API кључа.
TL;DR
  • Генеришите потписане токене за отпремање на серверској страни тако да прегледачи и мобилне апликације никада не виде ваш сирови API кључ.
  • Токени носе временску ознаку истицања и ограничења коришћења, ограничавајући штету ако икада буду извучени.
  • Директна отпремања са клијента на IPFS путем токена у потпуности уклањају уска грла серверског пропусног опсега.
  • Криптографски потписи на сваком токену спречавају манипулацију или фалсификовање лажног акредитива за отпремање.

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

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

IPFS Ninja

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

ПриступAPI кључ у клијенту?Аутоматски истиче?Фине границе?
Уграђени пуни API кључДа (несигурно)НеНе
Серверски проксиНеН/АН/А
Токен IPFS.NINJAНеДаДа

Ризик од излагања 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 };
}

Конфигурисање обима токена#

Приликом генерисања токена подесите expiresIn, maxUploads, maxSizeMB, allowedMimeTypes и ipWhitelist да бисте ограничили шта клијент са тим токеном може да уради:

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

Свако од ових ограничења се примењује на серверу приликом сваког отпремања, тако да је нападач — чак и ако извуче токен из клијентског кода — ограничен управо на ове границе, све док токен не истекне.

Интеграција са мобилним апликацијама#

Мобилне апликације (iOS, Android, React Native) захтевају токен директно од вашег сопственог бекенда — никада директно од IPFS API-ја — и користе га за директно отпремање. Бекенд задржава API кључ, а мобилна апликација добија само краткотрајни потписани токен. То омогућава апликацији да отпрема датотеке директно, без преусмеравања преко вашег сервера, чиме се штеди пропусни опсег и смањује кашњење, а API кључ никада не напушта бекенд.

Закључак#

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

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

Nacho Coll

О аутору

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.

Назад на Блог

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