IPFS Upload Token: อัปโหลด Client อย่างปลอดภัย
เรียนรู้วิธีที่ signed upload token ช่วยให้คุณอัปโหลดไปยัง IPFS จากเบราว์เซอร์และแอปมือถือได้อย่างปลอดภัยโดยไม่ต้องเปิดเผย API key ของคุณ
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.

การสร้างเว็บแอปพลิเคชันสมัยใหม่มักต้องการอัปโหลดไฟล์จากเบราว์เซอร์ของผู้ใช้ไปยังคลาวด์สตอเรจโดยตรง อย่างไรก็ตาม เมื่อพูดถึงความปลอดภัยในการอัปโหลด IPFS นักพัฒนาต้องเผชิญกับปัญหาที่ท้าทาย: จะอนุญาตให้อัปโหลดฝั่ง client ได้อย่างไรโดยไม่เปิดเผย API key ที่มีค่าของคุณ?
บริการ IPFS pinning ส่วนใหญ่บังคับให้คุณเลือกทางที่ไม่สะดวก: จัดการอัปโหลดทั้งหมดฝั่ง server (สร้างคอขวดและความซับซ้อน) หรือฝัง API key ใน client code (ฝันร้ายด้านความปลอดภัย) IPFS.NINJA แก้ปัญหานี้ด้วยฟีเจอร์เฉพาะที่ไม่มีบริการ pinning อื่นใดมี: signed upload token

ปัญหาของ IPFS API Key แบบดั้งเดิม#
| วิธีการ | API Key ในฝั่ง Client? | หมดอายุอัตโนมัติ? | จำกัดสิทธิ์ละเอียด? |
|---|---|---|---|
| ฝัง API Key เต็มรูปแบบ | ใช่ (ไม่ปลอดภัย) | ไม่ | ไม่ |
| Server-side Proxy | ไม่ | ไม่เกี่ยวข้อง | ไม่เกี่ยวข้อง |
| IPFS.NINJA Upload Token | ไม่ | ใช่ | ใช่ |
ความเสี่ยงจากการเปิดเผย API Key#
การฝัง API key โดยตรงในเบราว์เซอร์ JavaScript หมายความว่าใครก็ได้สามารถดู source code ของคุณและดึง credentials ออกมา สิ่งนี้อาจนำไปสู่:
- การอัปโหลดที่ไม่ได้รับอนุญาตที่ใช้โควต้าสตอเรจของคุณ
- การใช้งานบัญชี pinning ในทางที่ผิด
- การละเมิดข้อกำหนดด้านความปลอดภัยในสภาพแวดล้อมองค์กร
คอขวดฝั่ง Server#
ทางเลือก — ส่งการอัปโหลดทั้งหมดผ่าน backend — สร้างปัญหาหลายประการ:
- ค่าแบนด์วิดท์เซิร์ฟเวอร์เพิ่มขึ้น
- ความล่าช้าสูงขึ้นสำหรับผู้ใช้
- ข้อกำหนดโครงสร้างพื้นฐานที่ซับซ้อนมากขึ้น
ความปลอดภัยของแอปมือถือ#
แอปมือถือก็เผชิญกับความท้าทายเดียวกัน โดย API key ที่เก็บไว้ใน app bundle สามารถถูกดึงออกมาผ่าน reverse engineering
แนะนำ IPFS Upload Token#
IPFS.NINJA signed upload token ให้ทางออกที่ปลอดภัย:
- เซิร์ฟเวอร์สร้าง token: backend ของคุณสร้าง token ที่มีเวลาจำกัดโดยใช้ API key
- Client รับ token: token ถูกส่งอย่างปลอดภัยไปยังแอปพลิเคชัน frontend
- อัปโหลดโดยตรง: client อัปโหลดโดยตรงไปยัง IPFS.NINJA โดยใช้ signed token
- หมดอายุอัตโนมัติ: token หมดอายุหลังจากระยะเวลาที่กำหนด
ทำความเข้าใจความปลอดภัยของ Upload Token#
แต่ละ token ประกอบด้วย:
- Timestamp หมดอายุ: การยกเลิกอัตโนมัติหลังระยะเวลาที่กำหนด
- ข้อจำกัดการใช้งาน: จำกัดจำนวนไฟล์หรือขนาดรวม (ตัวเลือก)
- ลายเซ็นดิจิทัล: ป้องกันการแก้ไขหรือปลอมแปลง
- การยืนยันผู้ออก: เชื่อมกลับไปยังบัญชีที่ผ่านการรับรองของคุณ
การใช้งาน Backend: ตัวอย่าง 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', // 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' });
}
});
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'); });การใช้งาน Frontend: อัปโหลด Client อย่างปลอดภัย#
<!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>ข้อควรพิจารณาด้านความปลอดภัยขั้นสูง#
การจำกัดขอบเขต 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'] })
});การตรวจสอบเนื้อหา#
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' }); }
});Rate Limiting#
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);ข้อดีเหนือวิธีดั้งเดิม#
เทียบกับ Server-Side Proxy#
- ประสิทธิภาพ: อัปโหลดโดยตรงไม่ต้องใช้แบนด์วิดท์เซิร์ฟเวอร์
- ความสามารถในการขยาย: ไม่มีคอขวดเซิร์ฟเวอร์ในช่วงอัปโหลดสูง
- ต้นทุน: ลดค่าแบนด์วิดท์และการประมวลผล
เทียบกับ Client-Side API Key#
- ความปลอดภัย: ไม่มีความเสี่ยงจากการดึง API key ออก
- การปฏิบัติตาม: ผ่านข้อกำหนดการตรวจสอบความปลอดภัย
- การควบคุมการเข้าถึง: สิทธิ์ที่ละเอียดและหมดอายุอัตโนมัติ
เทียบกับบริการ Pinning อื่น#
IPFS.NINJA เป็นบริการ pinning รายใหญ่เพียงรายเดียวที่เสนอ signed upload token
ดูรายละเอียดเพิ่มเติมในคู่มือเปรียบเทียบฉบับสมบูรณ์
เคล็ดลับการ Deploy บน 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' })] });
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.' });
});การผสานกับ Framework ยอดนิยม#
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 };
}การกำหนดขอบเขตและอายุของ Token#
เมื่อสร้าง upload token คุณสามารถกำหนดขอบเขตให้แคบที่สุดเท่าที่จำเป็นสำหรับงานนั้น ๆ ได้ ด้วยพารามิเตอร์ต่อไปนี้:
- expiresIn — อายุของ token เช่น
'10m'สำหรับฟอร์มอัปโหลดครั้งเดียว หรือ'1h'สำหรับเซสชันที่ใช้งานต่อเนื่อง - maxUploads — จำนวนไฟล์สูงสุดที่ token นี้อัปโหลดได้ก่อนถูกปฏิเสธ
- maxSizeMB — ขนาดรวมสูงสุด (MB) ที่ token อนุญาตให้อัปโหลด
- allowedMimeTypes — จำกัดชนิดไฟล์ที่ยอมรับ เช่น
['image/jpeg', 'image/png']เพื่อป้องกันการอัปโหลดไฟล์ที่ไม่ต้องการ - ipWhitelist — จำกัดว่า token ใช้ได้เฉพาะจาก IP หรือช่วง CIDR ที่ระบุเท่านั้น
// สร้าง token ที่มีขอบเขตจำกัดสำหรับฟอร์มอัปโหลดรูปโปรไฟล์
const token = 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: '10m',
maxUploads: 1,
maxSizeMB: 5,
allowedMimeTypes: ['image/jpeg', 'image/png']
})
});ยิ่งขอบเขตแคบเท่าไร ความเสียหายที่เป็นไปได้จาก token ที่รั่วไหลก็ยิ่งน้อยลงเท่านั้น
การรวมเข้ากับแอปมือถือ#
แอป iOS, Android และ React Native เผชิญปัญหาเดียวกับเว็บแอป: หาก API key ถูกฝังไว้ใน app bundle ก็สามารถถูกดึงออกมาได้ด้วย reverse engineering รูปแบบที่ปลอดภัยคือให้แอปมือถือขอ upload token จาก backend ของคุณเองก่อนทุกครั้งที่ต้องอัปโหลด แล้วจึงใช้ token นั้นอัปโหลดตรงไปยัง IPFS.NINJA
ขั้นตอนทั่วไป:
- ผู้ใช้ล็อกอินในแอป และแอปเรียก endpoint ของ backend ตัวเอง (เช่น
/api/generate-upload-token) พร้อมส่ง session token เพื่อยืนยันตัวตน - Backend ตรวจสอบสิทธิ์ผู้ใช้ แล้วเรียก
https://api.ipfs.ninja/upload-tokensด้วย API key ที่เก็บไว้ฝั่งเซิร์ฟเวอร์เท่านั้น - Backend ส่ง upload token ที่ได้กลับไปให้แอปมือถือ
- แอปมือถืออัปโหลดไฟล์ตรงไปยัง
https://api.ipfs.ninja/upload/newโดยใช้ headerAuthorization: Signed <token>
ด้วยรูปแบบนี้ API key หลักจะไม่ปรากฏในแอปที่ติดตั้งบนอุปกรณ์ของผู้ใช้เลย ไม่ว่าจะเป็น iOS, Android หรือ React Native
สรุป#
Signed upload token แก้ปัญหาความปลอดภัยที่สำคัญในการพัฒนาแอปพลิเคชันแบบกระจายศูนย์ โดยให้วิธีการที่ปลอดภัยในการเปิดใช้งานการอัปโหลดโดยตรงจาก client ไปยัง IPFS โดยไม่ต้องเปิดเผย API key
ไม่ว่าคุณจะกำลังสร้างระบบจัดการเนื้อหา, NFT marketplace หรือแอปพลิเคชันใดก็ตามที่ต้องการการอัปโหลดไฟล์ที่ปลอดภัย upload token ของ IPFS.NINJA ให้ความปลอดภัยและความยืดหยุ่นที่คุณต้องการ
เรียนรู้เพิ่มเติมเกี่ยวกับพื้นฐาน IPFS ได้ที่คู่มือIPFS Pinning คืออะไร หรือสำรวจบทเรียน API ฉบับสมบูรณ์ของเรา
พร้อมเริ่ม pinning แล้วหรือยัง? สร้างบัญชีฟรี — 500 ไฟล์, 1 GB สตอเรจ, dedicated gateway ไม่ต้องใช้บัตรเครดิต
เกี่ยวกับบทความนี้
บทความนี้จัดทำด้วยความช่วยเหลือของ AI ตรวจสอบโดยมนุษย์ และยืนยันความถูกต้องกับแพลตฟอร์ม IPFS.NINJA จริงก่อนเผยแพร่ ดูวิธีที่เราใช้ AI ในเนื้อหาของเรา .

เกี่ยวกับผู้เขียน
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.
