· Nacho Coll · Guides · 17 분 소요
IPFS 업로드 토큰: API 키 노출 없이 안전한 클라이언트 사이드 업로드
서명된 업로드 토큰으로 API 키를 노출하지 않고 브라우저와 모바일 앱에서 안전하게 IPFS에 업로드하는 방법을 알아보세요.

현대 웹 애플리케이션을 구축할 때 사용자의 브라우저에서 클라우드 스토리지로 직접 파일을 업로드해야 하는 경우가 많습니다. 그러나 IPFS 업로드 보안과 관련하여 개발자들은 어려운 딜레마에 직면합니다: 소중한 API 키를 노출하지 않고 클라이언트 사이드 업로드를 어떻게 허용할 것인가?
대부분의 IPFS 피닝 서비스는 불편한 선택을 강요합니다: 모든 업로드를 서버 사이드에서 처리하거나(병목 현상과 복잡성 초래) API 키를 클라이언트 코드에 포함하거나(보안 악몽). IPFS.NINJA는 다른 피닝 서비스에는 없는 독특한 기능으로 이 문제를 해결합니다: 서명된 업로드 토큰.

기존 IPFS API 키의 문제점
IPFS에 업로드해야 하는 클라이언트 사이드 애플리케이션을 구축할 때 개발자들은 일반적으로 여러 보안 과제에 직면합니다:
API 키 노출 위험
브라우저 JavaScript에 API 키를 직접 삽입하면 누구나 소스 코드를 확인하고 자격 증명을 추출할 수 있습니다. 이로 인해 다음과 같은 문제가 발생할 수 있습니다:
- 무단 업로드로 인한 스토리지 할당량 소비
- 피닝 서비스 계정의 잠재적 남용
- 기업 환경에서의 보안 규정 준수 위반
서버 사이드 병목 현상
대안인 백엔드를 통한 모든 업로드 라우팅은 여러 문제를 야기합니다:
- 서버 대역폭 비용 증가
- 사용자 지연 시간 증가
- 더 복잡한 인프라 요구 사항
- 잠재적 단일 장애 지점
모바일 앱 보안
모바일 애플리케이션도 유사한 과제에 직면하며, 앱 번들에 저장된 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
};
}결론
서명된 업로드 토큰은 탈중앙화 애플리케이션 개발에서 중요한 보안 과제를 해결합니다. API 키를 노출하지 않고 클라이언트에서 IPFS로의 직접 업로드를 안전하게 실현하는 방법을 제공함으로써, 현대 웹 애플리케이션의 새로운 아키텍처 가능성을 열어줍니다.
콘텐츠 관리 시스템, NFT 마켓플레이스, 또는 안전한 파일 업로드가 필요한 모든 애플리케이션을 구축하든, IPFS.NINJA의 업로드 토큰은 필요한 보안과 유연성을 제공합니다. 구현은 간단하고, 보안 이점은 상당하며, 성능 향상은 눈에 띕니다.
IPFS 기본 사항에 대해 더 알아보려면 IPFS 피닝이란 무엇인가 가이드를 확인하거나 완전한 API 튜토리얼을 살펴보세요. 다양한 옵션을 평가하는 개발자를 위해 최고의 IPFS 피닝 서비스 비교가 포괄적인 인사이트를 제공합니다.
애플리케이션에 안전한 클라이언트 사이드 IPFS 업로드를 구현할 준비가 되셨나요? 현대적 보안 관행과 탈중앙화 스토리지의 조합은 보안 표준을 유지하면서 확장해야 하는 프로덕션 애플리케이션에 이상적인 접근 방식입니다.
피닝을 시작할 준비가 되셨나요? 무료 계정 만들기 — 500개 파일, 1 GB 스토리지, 전용 게이트웨이. 신용카드 불필요.
