· Nacho Coll · Guides · 10 хв читання
Токени завантаження 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 {
// 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');
});Реалізація на фронтенді: безпечне клієнтське завантаження
Тепер розглянемо фронтенд-код, який безпечно завантажує файли за допомогою підписаного токена:
<!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>Розширені аспекти безпеки
При реалізації безпеки завантаження в IPFS з підписаними токенами враховуйте наступні додаткові заходи:
Обмеження області дії токена
Налаштовуйте токени з відповідними обмеженнями:
// 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
})
});Валідація контенту
Завжди перевіряйте завантажений контент на вашому бекенді:
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' });
}
});Обмеження частоти запитів
Реалізуйте додаткове обмеження частоти запитів на ендпоінт генерації токенів:
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);Переваги порівняно з традиційними підходами
Підписані токени завантаження забезпечують низку переваг порівняно з альтернативними методами безпеки завантаження в IPFS:
Порівняно з проксуванням на стороні сервера
- Продуктивність: прямі завантаження виключають використання серверної пропускної здатності
- Масштабованість: відсутність серверних вузьких місць при високому навантаженні завантажень
- Вартість: зниження витрат на пропускну здатність та обробку
- Досвід користувача: вища швидкість завантаження та відстеження прогресу
Порівняно з API-ключами на стороні клієнта
- Безпека: немає ризику витягнення або зловживання API-ключем
- Відповідність вимогам: задовольняє вимоги аудиту безпеки
- Контроль доступу: детальні дозволи та автоматичне закінчення терміну дії
- Моніторинг: покращене відстеження джерел та патернів завантажень
Порівняно з іншими сервісами закріплення
IPFS.NINJA наразі є єдиним великим сервісом закріплення, що пропонує підписані токени завантаження. Конкуренти, такі як Pinata, вимагають або проксування на стороні сервера, або розкриття API-ключа на стороні клієнта, що робить цю функцію унікальною перевагою.
Детальніше про те, як IPFS.NINJA порівнюється з іншими сервісами, читайте в нашому докладному посібнику з порівняння.
Поради щодо розгортання у продакшені
При розгортанні підписаних токенів завантаження у продакшені:
Конфігурація середовища
Зберігайте конфіденційну конфігурацію безпечно:
// 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']
};Моніторинг та логування
Реалізуйте комплексне логування для моніторингу безпеки:
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
});Обробка помилок
Реалізуйте надійну обробку помилок, яка не розкриває конфіденційну інформацію:
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.'
});
});Інтеграція з популярними фреймворками
Підписані токени завантаження легко інтегруються з сучасними веб-фреймворками. Ось короткі приклади інтеграції:
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-ключів, вони відкривають нові архітектурні можливості для сучасних веб-додатків.
Незалежно від того, чи створюєте ви систему керування контентом, маркетплейс NFT або будь-який додаток, що потребує безпечного завантаження файлів, токени завантаження IPFS.NINJA забезпечують необхідну безпеку та гнучкість. Реалізація проста, переваги безпеки значні, а приріст продуктивності суттєвий.
Щоб дізнатися більше про основи IPFS, ознайомтеся з нашим посібником що таке закріплення IPFS або вивчіть наш повний підручник з API. Для розробників, що оцінюють різні варіанти, наше порівняння найкращих сервісів закріплення IPFS надасть вичерпну інформацію.
Готові реалізувати безпечне клієнтське завантаження в IPFS у вашому додатку? Поєднання сучасних практик безпеки та децентралізованого сховища робить цей підхід ідеальним для продакшен-додатків, яким потрібно масштабуватися, зберігаючи стандарти безпеки.
Готові почати закріплення? Створіть безкоштовний акаунт — 500 файлів, 1 ГБ сховища, виділений шлюз. Кредитна картка не потрібна.
