· Nacho Coll · Guides · 9 мин четене
Токени за качване в IPFS: Сигурно качване от страна на клиента без разкриване на API ключове
Научете как подписаните токени за качване ви позволяват безопасно да качвате в IPFS от браузъри и мобилни приложения, без да разкривате API ключа си.

Разработката на съвременни уеб приложения често изисква качване на файлове директно от браузърите на потребителите в облачно хранилище. Когато обаче става въпрос за сигурност на качването в IPFS, разработчиците се сблъскват с трудна дилема: как да разрешите качване от страна на клиента, без да разкривате ценните си API ключове за потенциална злоупотреба?
Повечето услуги за закрепване в IPFS ви поставят пред неудобен избор: или да обработвате всички качвания от страна на сървъра (създавайки тесни места и сложност), или да вградите API ключа си в клиентския код (кошмар за сигурността). IPFS.NINJA решава това с уникална функция, която никоя друга услуга за закрепване не предлага: подписани токени за качване.

Проблемът с традиционните API ключове за IPFS
При създаването на клиентски приложения, които трябва да качват в IPFS, разработчиците обикновено срещат няколко предизвикателства за сигурност:
Риск от разкриване на API ключ
Вграждането на API ключове директно в JavaScript на браузъра означава, че всеки може да прегледа изходния ви код и да извлече данните за достъп. Това може да доведе до:
- Неоторизирани качвания, които изразходват квотата ви за съхранение
- Потенциална злоупотреба с акаунта ви за услугата за закрепване
- Нарушения на изискванията за сигурност в корпоративни среди
Тесни места на сървъра
Алтернативата — маршрутизиране на всички качвания през бекенда ви — създава редица проблеми:
- Увеличени разходи за сървърна честотна лента
- По-голямо забавяне за потребителите
- По-сложни изисквания за инфраструктура
- Потенциални единични точки на отказ
Сигурност на мобилни приложения
Мобилните приложения се сблъскват с подобни предизвикателства: API ключовете, съхранявани в пакетите на приложенията, могат да бъдат извлечени чрез обратно инженерство.
Представяме токените за качване в IPFS
Подписаните токени за качване на IPFS.NINJA осигуряват сигурен компромис. Ето как работят:
- Сървърът генерира токен: бекендът ви създава ограничен по време, подписан токен с помощта на API ключа ви
- Клиентът получава токена: токенът се предава безопасно на фронтенд приложението ви
- Директно качване: клиентите качват директно в IPFS.NINJA, използвайки подписания токен
- Автоматично изтичане: токените изтичат след зададен период, ограничавайки прозореца на уязвимост
Този подход съчетава сигурността на сървърната автентикация с предимствата на директното клиентско качване.
Разбиране на сигурността на токените за качване
Подписаните токени за качване използват криптографски подписи за осигуряване на автентичност без разкриване на основния ви API ключ. Всеки токен съдържа:
- Времева марка на изтичане: автоматично анулиране след определен период
- Ограничения на употребата: опционални лимити за брой файлове или общ размер
- Криптографски подпис: предотвратява подправяне или фалшификация
- Верификация на издателя: връзка с автентикирания ви акаунт
За разлика от API ключовете, токените за качване са проектирани за безопасно вграждане в клиентски код. Дори ако бъдат извлечени, те предоставят ограничен достъп, който автоматично изтича.
Реализация на бекенд: пример с Express.js
Нека изградим пълен пример, показващ как да реализирате сигурни клиентски качвания в IPFS. Първо, Express.js бекендът, генериращ токени за качване:
// 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 {
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>Разширени съображения за сигурност
Ограничения на обхвата на токена
// 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',
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 е единствената голяма услуга за закрепване, предлагаща подписани токени за качване. Конкурентите изискват или проксиране от страна на сървъра, или разкриване на API ключ от страна на клиента.
За повече подробности вижте нашето подробно ръководство за сравнение.
Съвети за внедряване в продукция
Конфигурация на средата
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' })]
});
logger.info('Upload token generated', {
userId: req.user.id, clientIP: req.ip, userAgent: req.get('User-Agent'), expiresAt: tokenData.expiresAt
});Обработка на грешки
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(); /* 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 };
}Заключение
Подписаните токени за качване решават критичен проблем за сигурността в разработката на децентрализирани приложения. Предоставяйки сигурен начин за директно клиентско качване в IPFS без разкриване на API ключове, те отварят нови архитектурни възможности за съвременни уеб приложения.
Научете повече в нашето ръководство какво е IPFS закрепване или разгледайте пълния урок за API. За сравнение вижте най-добрите услуги за закрепване IPFS.
Готови ли сте да започнете закрепване? Създайте безплатен акаунт — 500 файла, 1 GB хранилище, специализиран шлюз. Не се изисква кредитна карта.
