· Nacho Coll · Guides · 9 phút đọc
IPFS Upload Token: Tải lên phía Client an toàn mà không cần lộ API Key
Tìm hiểu cách signed upload token cho phép bạn tải lên IPFS an toàn từ trình duyệt và ứng dụng di động mà không cần lộ API key.

Việc xây dựng ứng dụng web hiện đại thường yêu cầu tải file trực tiếp từ trình duyệt người dùng lên cloud storage. Tuy nhiên, khi nói đến bảo mật upload IPFS, các nhà phát triển đối mặt với một thách thức khó khăn: làm thế nào để cho phép upload phía client mà không lộ API key quý giá?
Hầu hết các dịch vụ IPFS pinning buộc bạn phải lựa chọn không thoải mái: xử lý tất cả upload phía server (tạo bottleneck và phức tạp) hoặc nhúng API key trong client code (cơn ác mộng bảo mật). IPFS.NINJA giải quyết vấn đề này bằng một tính năng độc đáo mà không dịch vụ pinning nào khác có: signed upload token.

Vấn đề với API Key IPFS truyền thống
Rủi ro lộ API Key
Nhúng API key trực tiếp trong JavaScript trình duyệt nghĩa là ai cũng có thể xem source code và trích xuất thông tin xác thực. Điều này có thể dẫn đến:
- Upload trái phép tiêu thụ quota lưu trữ
- Lạm dụng tài khoản dịch vụ pinning
- Vi phạm tuân thủ bảo mật trong môi trường doanh nghiệp
Bottleneck phía Server
Phương án thay thế — định tuyến tất cả upload qua backend — tạo ra nhiều vấn đề:
- Chi phí bandwidth server tăng
- Độ trễ cao hơn cho người dùng
- Yêu cầu hạ tầng phức tạp hơn
Bảo mật ứng dụng di động
Ứng dụng di động cũng gặp thách thức tương tự, API key lưu trong app bundle có thể bị trích xuất qua reverse engineering.
Giới thiệu IPFS Upload Token
Signed upload token của IPFS.NINJA cung cấp giải pháp trung gian an toàn:
- Server tạo token: Backend tạo token có giới hạn thời gian, được ký bằng API key
- Client nhận token: Token được truyền an toàn đến ứng dụng frontend
- Upload trực tiếp: Client upload trực tiếp lên IPFS.NINJA bằng signed token
- Hết hạn tự động: Token hết hạn sau thời gian đã thiết lập
Hiểu về bảo mật Upload Token
Mỗi token chứa:
- Timestamp hết hạn: Tự động vô hiệu hóa sau thời gian chỉ định
- Giới hạn sử dụng: Giới hạn tùy chọn về số file hoặc tổng kích thước
- Chữ ký số: Ngăn chặn giả mạo
- Xác minh người phát hành: Liên kết về tài khoản đã xác thực
Triển khai Backend: Ví dụ 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'); });Triển khai Frontend: Upload Client an toàn
<!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>Các cân nhắc bảo mật nâng cao
Giới hạn phạm vi Token
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'] })
});Xác thực nội dung
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' }); }
});Giới hạn tốc độ
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);Ưu điểm so với phương pháp truyền thống
So với Server-Side Proxy
- Hiệu suất: Upload trực tiếp loại bỏ việc sử dụng bandwidth server
- Khả năng mở rộng: Không có bottleneck server trong thời gian upload cao
- Chi phí: Giảm chi phí bandwidth và xử lý
So với Client-Side API Key
- Bảo mật: Không có rủi ro lộ API key
- Tuân thủ: Đáp ứng yêu cầu kiểm toán bảo mật
- Kiểm soát truy cập: Quyền hạn chi tiết và hết hạn tự động
So với dịch vụ Pinning khác
IPFS.NINJA hiện là dịch vụ pinning lớn duy nhất cung cấp signed upload token.
Xem chi tiết tại hướng dẫn so sánh toàn diện.
Mẹo triển khai Production
Cấu hình môi trường
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']
};Giám sát và ghi log
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 });Xử lý lỗi
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.' });
});Tích hợp với Framework phổ biến
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 };
}Kết luận
Signed upload token giải quyết thách thức bảo mật quan trọng trong phát triển ứng dụng phi tập trung. Bằng cách cung cấp phương pháp an toàn để cho phép upload trực tiếp từ client lên IPFS mà không lộ API key, chúng mở ra khả năng kiến trúc mới cho ứng dụng web hiện đại.
Tìm hiểu thêm về nền tảng IPFS tại hướng dẫn IPFS Pinning là gì hoặc khám phá hướng dẫn API đầy đủ. Để so sánh các lựa chọn, xem so sánh dịch vụ IPFS pinning tốt nhất.
Sẵn sàng bắt đầu pinning? Tạo tài khoản miễn phí — 500 file, 1 GB lưu trữ, dedicated gateway. Không cần thẻ tín dụng.
