IPFS-lataustokenit: turvalliset asiakaslataukset
Opi miten allekirjoitetut lataustokenit mahdollistavat turvallisen IPFS-latauksen selaimista ja mobiilisovelluksista paljastamatta API-avainta.
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.

- Luo allekirjoitetut lataustokenit palvelinpuolella, jotta selaimet ja mobiilisovellukset eivät koskaan näe raakaa API-avaintasi.
- Tokeneilla on vanhenemisaikaleima ja käyttörajoitukset, mikä rajoittaa vahinkoa, jos yksi niistä joskus vuotaa.
- Suorat asiakkaalta-IPFS:ään-lataukset tokenien kautta poistavat palvelimen kaistanleveyspullonkaulat kokonaan.
- Kryptografiset allekirjoitukset jokaisessa tokenissa estävät peukaloinnin tai väärennetyn latausvaltuutuksen luomisen.
Modernien verkkosovellusten rakentaminen vaatii usein tiedostojen lataamista suoraan käyttäjien selaimista pilvipalveluun. IPFS-latausten turvallisuuden osalta kehittäjät kohtaavat kuitenkin haastavan dilemman: miten sallia asiakaspuolen lataukset paljastamatta arvokkaita API-avaimia mahdolliselle väärinkäytölle?
Useimmat IPFS pinning -palvelut pakottavat sinut epämukavaan valintaan: joko käsittelet kaikki lataukset palvelinpuolella (luoden pullonkauloja ja monimutkaisuutta) tai upottelet API-avaimesi asiakaskoodiin (turvallisuuspainajaiseen). IPFS.NINJA ratkaisee tämän ainutlaatuisella ominaisuudella, jota mikään muu pinning-palvelu ei tarjoa: allekirjoitetut lataustokenit.

Miten IPFS-lataustokenit estävät API-avainten paljastumisen: vertailu#
| Lähestymistapa | API-avain asiakkaalla? | Vanhenee automaattisesti? | Tarkat rajoitukset? |
|---|---|---|---|
| API-avaimen upottaminen | Kyllä (epäturvallinen) | Ei | Ei |
| Palvelinpuolen välityspalvelin | Ei | Ei sovelleta | Ei sovelleta |
| IPFS.NINJA-lataustoken | Ei | Kyllä | Kyllä |
API-avaimen paljastumisen riski#
API-avainten upottaminen suoraan selaimen JavaScriptiin tarkoittaa, että kuka tahansa voi tarkastella lähdekoodiasi ja poimia tunnistetietosi. Tämä voi johtaa:
- Luvattomiin latauksiin, jotka kuluttavat tallennuskiintiötäsi
- Pinning-palvelutilisi mahdolliseen väärinkäyttöön
- Turvallisuusvaatimusten rikkomiseen yritysympäristöissä
Palvelinpuolen pullonkaulat#
Vaihtoehto --- kaikkien latausten reititys backendin kautta --- luo useita ongelmia:
- Lisääntyneet palvelimen kaistanleveyskulut
- Korkeampi viive käyttäjille
- Monimutkaisemmat infrastruktuurivaatimukset
- Mahdolliset yksittäiset vikapisteet
Mobiilisovellusten turvallisuus#
Mobiilisovellukset kohtaavat vastaavia haasteita --- sovelluspaketteihin tallennetut API-avaimet voidaan purkaa käänteismekaniikalla.
IPFS-lataustokenien esittely#
IPFS.NINJA:n allekirjoitetut lataustokenit tarjoavat turvallisen kompromissin. Näin ne toimivat:
- Palvelin luo tokenin: Backendisi luo aikaan sidotun, allekirjoitetun tokenin API-avaimellasi
- Asiakas vastaanottaa tokenin: Token siirretään turvallisesti frontend-sovellukseesi
- Suora lataus: Asiakkaat lataavat suoraan IPFS.NINJA:lle allekirjoitetulla tokenilla
- Automaattinen vanheneminen: Tokenit vanhenevat asetetun ajan jälkeen, rajoittaen altistumisikkunaa
Lataustokenien turvallisuuden ymmärtäminen#
Allekirjoitetut lataustokenit käyttävät kryptografisia allekirjoituksia aitouden varmistamiseksi paljastamatta pää-API-avaintasi. Jokainen token sisältää:
- Vanhenemisaikaleima: Automaattinen mitätöinti määritetyn ajan jälkeen
- Käyttörajoitukset: Valinnaiset rajoitukset tiedostomäärälle tai kokonaiskoolle
- Kryptografinen allekirjoitus: Estää manipuloinnin tai väärentämisen
- Myöntäjän vahvistus: Linkki todennettuun tiliisi
Backend-toteutus: Express.js-esimerkki#
// 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'); });Frontend-toteutus: turvallinen asiakaspuolen lataus#
<!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>Edistyneet turvallisuusnäkökohdat#
Tokenin laajuusrajoitukset#
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'] })
});Sisällön validointi#
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' }); }
});Pyyntöjen nopeusrajoitus#
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);Token-vanhentumisstrategiat#
Valitse tokenin voimassaoloaika käyttötapauksen mukaan: lyhytikäiset tokenit (15-30 min) sopivat kertaluonteisiin latauksiin, kuten yksittäisen profiilikuvan lataamiseen, koska ne minimoivat ajan, jonka aikana vuotanut token olisi käyttökelpoinen. Pidempiaikaiset tokenit (1-2h) sopivat eräajolatauksiin, joissa käyttäjä lataa useita tiedostoja peräkkäin eikä sovellus voi pyytää uutta tokenia jokaista tiedostoa varten ilman huonoa käyttökokemusta.
Toteuta automaattinen tokenin uusiminen asiakaspuolella: kun palvelin palauttaa 401-virheen vanhentuneesta tokenista, asiakas pyytää läpinäkyvästi uuden tokenin taustalla ja toistaa epäonnistuneen latauksen ilman käyttäjän keskeytystä. Näin vältetään tilanteet, joissa käyttäjän lataus epäonnistuu kesken kaiken vain siksi, että token ehti vanhentua.
Mobiilikehitys ja tokenin hallinta#
Mobiilisovelluksissa (iOS, Android, React Native) tokenit tulee säilyttää vain muistissa, ei pysyvässä tallennustilassa kuten AsyncStoragessa tai Keychainissa, koska token on jo luonnostaan lyhytikäinen eikä sitä ole tarkoitettu säilytettäväksi istuntojen välillä. Kun sovellus palaa taustalta tai verkkoyhteys palautuu katkoksen jälkeen, tarkista onko token yhä voimassa ja pyydä tarvittaessa uusi ennen seuraavaa latausta. React Nativessa tämä kannattaa toteuttaa yhtenä hookina, joka kapseloi sekä tokenin haun että sen automaattisen uusimisen, jotta latauskomponentit eivät joudu käsittelemään vanhentumislogiikkaa erikseen.
Edut perinteisiin lähestymistapoihin verrattuna#
Palvelinpuolen proxy-palvelimeen verrattuna#
- Suorituskyky: Suorat lataukset poistavat palvelimen kaistanleveyden käytön
- Skaalautuvuus: Ei palvelinpullonkauloja korkean latauskuorman aikana
- Kustannus: Vähennetyt kaistanleveys- ja käsittelykustannukset
- Käyttäjäkokemus: Paremmat latausnopeudet ja edistymisen seuranta
Asiakaspuolen API-avaimiin verrattuna#
- Turvallisuus: Ei riskiä API-avaimen poiminnasta tai väärinkäytöstä
- Vaatimustenmukaisuus: Täyttää turvallisuustarkastusten vaatimukset
- Pääsynhallinta: Hienojakoiset käyttöoikeudet ja automaattinen vanheneminen
- Seuranta: Parempi latauslähteiden ja -mallien seuranta
Muihin pinning-palveluihin verrattuna#
IPFS.NINJA on tällä hetkellä ainoa merkittävä pinning-palvelu, joka tarjoaa allekirjoitettuja lataustokeneja. Lisätietoja löydät kattavasta vertailuoppaastamme.
Tuotantoympäristön käyttöönottovinkkejä#
Ympäristökonfiguraatio#
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']
};Seuranta ja lokitus#
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 });Virheenkäsittely#
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.' });
});Integraatio suosittujen kehysten kanssa#
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 };
}Yhteenveto#
Allekirjoitetut lataustokenit ratkaisevat kriittisen turvallisuushaasteen hajautettujen sovellusten kehityksessä. Tarjoamalla turvallisen tavan mahdollistaa suorat asiakaspuolen lataukset IPFS:ään ilman API-avainten paljastamista ne avaavat uusia arkkitehtuurillisia mahdollisuuksia moderneille verkkosovelluksille.
Lisätietoja IPFS:n perusteista löydät oppaastamme Mikä on IPFS pinning tai tutustu täydelliseen API-opetusohjelmaamme. Kehittäjille, jotka arvioivat eri vaihtoehtoja, parhaat IPFS pinning -palvelut -vertailumme tarjoaa kattavan näkemyksen.
Valmis aloittamaan pinningin? Luo ilmainen tili — 500 tiedostoa, 1 GB tallennustilaa, dedikoitu gateway. Luottokorttia ei tarvita.
Tietoa tästä artikkelista
Tämä artikkeli on tekoälyavusteinen, ihmisen tarkistama ja tuoteverifioitu IPFS.NINJA-alustaa vasten ennen julkaisua. Lue, miten käytämme tekoälyä sisällössämme .

Tietoa kirjoittajasta
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.
