9. Practical Code Templates
9.1 Frontend Direct Upload to Object Storage (JavaScript)
javascript
/**
* Object Storage Direct Upload Utility
* Supports: Alibaba Cloud OSS, Tencent Cloud COS, AWS S3
*/
class DirectUploader {
constructor(config) {
this.provider = config.provider // 'oss' | 'cos' | 's3'
this.region = config.region
this.bucket = config.bucket
this.getCredentials = config.getCredentials // Function to fetch temporary credentials
}
/**
* Fetch STS temporary credentials
*/
async fetchCredentials() {
// Request temporary credentials from backend
const credentials = await this.getCredentials()
return {
accessKeyId: credentials.accessKeyId,
accessKeySecret: credentials.accessKeySecret,
sessionToken: credentials.securityToken || credentials.sessionToken,
expiration: credentials.expiration
}
}
/**
* Generate upload signature (for client-side signature computation)
*/
generateSignature(credentials, fileKey, fileType, options = {}) {
const timestamp = new Date().toISOString()
const date = timestamp.slice(0, 10).replace(/-/g, '')
// Signature algorithms differ slightly by provider
switch (this.provider) {
case 'oss':
return this._ossSignature(credentials, fileKey, date, options)
case 'cos':
return this._cosSignature(credentials, fileKey, date, options)
case 's3':
return this._s3Signature(credentials, fileKey, date, options)
default:
throw new Error('Unknown provider')
}
}
/**
* Single file upload (small files < 100MB)
*/
async upload(file, options = {}) {
const credentials = await this.fetchCredentials()
const fileKey = this._generateFileKey(file, options.directory)
const formData = new FormData()
// Build form fields (field names differ by provider)
const formFields = this._buildFormFields(
credentials,
fileKey,
file.type,
options
)
Object.entries(formFields).forEach(([key, value]) => {
formData.append(key, value)
})
formData.append('file', file)
// Send upload request
const uploadUrl = this._getUploadUrl()
const response = await fetch(uploadUrl, {
method: 'POST',
body: formData,
// For large files, you may need a longer timeout
signal: options.signal // Support AbortController to cancel upload
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Upload failed: ${response.status} ${errorText}`)
}
return {
url: this._getFileUrl(fileKey),
key: fileKey,
etag: response.headers.get('ETag'),
size: file.size
}
}
/**
* Multipart upload (large files > 100MB)
*/
async multipartUpload(file, options = {}) {
const partSize = options.partSize || 10 * 1024 * 1024 // Default 10MB per part
const parallel = options.parallel || 3 // Default 3 concurrent
const credentials = await this.fetchCredentials()
const fileKey = this._generateFileKey(file, options.directory)
// 1. Initialize multipart upload
const uploadId = await this._initMultipartUpload(
credentials,
fileKey,
file.type
)
// 2. Calculate parts
const parts = []
const totalParts = Math.ceil(file.size / partSize)
for (let i = 0; i < totalParts; i++) {
const start = i * partSize
const end = Math.min(start + partSize, file.size)
parts.push({
number: i + 1,
start,
end,
blob: file.slice(start, end)
})
}
// 3. Upload parts (with concurrency control and resumable upload)
const uploadedParts = []
const failedParts = []
// Support resumable upload: check which parts are already uploaded
if (options.resume) {
const existingParts = await this._listParts(
credentials,
fileKey,
uploadId
)
for (const part of existingParts) {
uploadedParts.push(part)
}
}
// Filter out already uploaded parts
const pendingParts = parts.filter(
(p) => !uploadedParts.some((up) => up.partNumber === p.number)
)
// Concurrent upload
const uploadPart = async (part) => {
try {
const etag = await this._uploadPart(
credentials,
fileKey,
uploadId,
part
)
return { partNumber: part.number, etag }
} catch (error) {
failedParts.push({ part, error })
throw error
}
}
// Use Promise.all to control concurrency
const chunks = []
for (let i = 0; i < pendingParts.length; i += parallel) {
chunks.push(pendingParts.slice(i, i + parallel))
}
for (const chunk of chunks) {
const results = await Promise.allSettled(chunk.map(uploadPart))
for (const result of results) {
if (result.status === 'fulfilled') {
uploadedParts.push(result.value)
}
}
}
// Check if all parts uploaded successfully
if (uploadedParts.length !== totalParts) {
throw new Error(
`Upload incomplete: ${uploadedParts.length}/${totalParts} parts uploaded`
)
}
// 4. Complete multipart upload (merge parts)
await this._completeMultipartUpload(
credentials,
fileKey,
uploadId,
uploadedParts
)
return {
url: this._getFileUrl(fileKey),
key: fileKey,
size: file.size,
parts: totalParts
}
}
/**
* Generate file storage path
*/
_generateFileKey(file, directory = '') {
const date = new Date()
const datePath = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
const random = Math.random().toString(36).substring(2, 10)
const ext = file.name.split('.').pop() || 'bin'
const key = directory
? `${directory}/${datePath}/${random}.${ext}`
: `${datePath}/${random}.${ext}`
return key
}
// ============ Provider-Specific Methods ============
_getUploadUrl() {
switch (this.provider) {
case 'oss':
return `https://${this.bucket}.oss-${this.region}.aliyuncs.com`
case 'cos':
return `https://${this.bucket}.cos.${this.region}.myqcloud.com`
case 's3':
return `https://${this.bucket}.s3.${this.region}.amazonaws.com`
default:
throw new Error('Unknown provider')
}
}
_getFileUrl(key) {
return `https://${this.bucket}.${this.provider === 'oss' ? 'oss' : 'cos'}-${this.region}.${
this.provider === 'oss'
? 'aliyuncs.com'
: this.provider === 'cos'
? 'myqcloud.com'
: 'amazonaws.com'
}/${key}`
}
// Provider-specific signature, multipart upload methods... (implement as needed)
_buildFormFields(credentials, fileKey, fileType, options) {
// Provider-specific form field construction logic
// Implement according to each provider's documentation
return {}
}
async _initMultipartUpload(credentials, fileKey, fileType) {
// Provider-specific multipart upload initialization logic
return 'upload-id'
}
async _uploadPart(credentials, fileKey, uploadId, part) {
// Provider-specific part upload logic
return 'etag'
}
async _completeMultipartUpload(credentials, fileKey, uploadId, parts) {
// Provider-specific multipart upload completion logic
}
async _listParts(credentials, fileKey, uploadId) {
// Provider-specific logic to list uploaded parts
return []
}
}
// Usage example
const uploader = new DirectUploader({
provider: 'oss',
region: 'cn-beijing',
bucket: 'myapp-images-prod',
getCredentials: async () => {
// Request temporary credentials from backend
const res = await fetch('/api/upload/credentials')
return res.json()
}
})
// Small file upload
async function uploadAvatar(file) {
try {
const result = await uploader.upload(file, {
directory: 'avatars',
onProgress: (progress) => {
console.log(`Upload progress: ${progress.percent}%`)
}
})
console.log('Upload successful:', result.url)
return result
} catch (error) {
console.error('Upload failed:', error)
throw error
}
}
// Large file multipart upload
async function uploadVideo(file) {
try {
const result = await uploader.multipartUpload(file, {
directory: 'videos',
partSize: 10 * 1024 * 1024, // 10MB per part
parallel: 3, // 3 concurrent
resume: true, // Support resumable upload
onProgress: (progress) => {
console.log(
`Upload progress: ${progress.percent}%, uploaded ${progress.loaded}/${progress.total}`
)
},
onPartComplete: (part) => {
console.log(`Part ${part.number} upload complete`)
}
})
console.log('Upload successful:', result.url)
return result
} catch (error) {
console.error('Upload failed:', error)
// You can implement retry logic or save checkpoint info here
throw error
}
}9.2 Backend Temporary Credential Service (Node.js/Express)
javascript
/**
* Object Storage STS Temporary Credential Service
* Supports: Alibaba Cloud OSS, Tencent Cloud COS, AWS S3
*/
const express = require('express')
const STS = require('ali-oss').STS // Alibaba Cloud
// const COS = require('cos-nodejs-sdk-v5') // Tencent Cloud
const router = express.Router()
// Configuration
const config = {
// Alibaba Cloud OSS configuration
oss: {
accessKeyId: process.env.OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
region: 'oss-cn-beijing',
bucket: 'myapp-images-prod',
// STS role ARN (needs to be created in RAM console)
roleArn: process.env.OSS_STS_ROLE_ARN
}
}
/**
* Get STS temporary credentials (Alibaba Cloud OSS)
* POST /api/upload/credentials
*/
router.post('/credentials', async (req, res) => {
try {
// 1. Verify user identity (implement as needed)
const userId = req.user?.id
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' })
}
// 2. Generate unique file path prefix (for permission isolation)
const date = new Date()
const prefix = `uploads/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${userId}/`
// 3. Create STS client
const sts = new STS({
accessKeyId: config.oss.accessKeyId,
accessKeySecret: config.oss.accessKeySecret
})
// 4. Request temporary credentials
const result = await sts.assumeRole(
config.oss.roleArn,
{
// Policy restricts permission scope (principle of least privilege)
Statement: [
{
Effect: 'Allow',
Action: [
'oss:PutObject',
'oss:InitiateMultipartUpload',
'oss:UploadPart',
'oss:CompleteMultipartUpload',
'oss:AbortMultipartUpload',
'oss:ListParts'
],
Resource: [`acs:oss:*:*:${config.oss.bucket}/${prefix}*`]
}
],
Version: '1'
},
3600, // Credential validity: 1 hour
'web-upload-session-' + Date.now()
)
// 5. Return credentials and configuration
res.json({
success: true,
data: {
// STS temporary credentials
credentials: {
accessKeyId: result.credentials.AccessKeyId,
accessKeySecret: result.credentials.AccessKeySecret,
sessionToken: result.credentials.SecurityToken,
expiration: result.credentials.Expiration
},
// Upload configuration
config: {
provider: 'oss',
region: config.oss.region,
bucket: config.oss.bucket,
endpoint: `https://${config.oss.bucket}.${config.oss.region}.aliyuncs.com`,
prefix: prefix, // File path prefix
// Security limits
maxSize: 100 * 1024 * 1024, // Max 100MB
allowedTypes: [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'video/mp4'
]
}
}
})
} catch (error) {
console.error('Get credentials failed:', error)
res.status(500).json({
success: false,
error: 'Failed to get upload credentials',
message: error.message
})
}
})
/**
* Callback notification: frontend notifies backend after upload completes
* POST /api/upload/callback
*/
router.post('/callback', async (req, res) => {
try {
const { key, etag, size, mimeType, originalName } = req.body
const userId = req.user?.id
// 1. Verify file existence
// 2. Save file info to database
const fileRecord = await db.files.create({
userId,
key,
etag,
size,
mimeType,
originalName,
url: `https://cdn.example.com/${key}`,
createdAt: new Date()
})
// 3. Async processing: generate thumbnails, extract metadata, content moderation, etc.
await processFileAsync(fileRecord)
res.json({
success: true,
data: {
fileId: fileRecord.id,
url: fileRecord.url,
size: fileRecord.size
}
})
} catch (error) {
console.error('Upload callback failed:', error)
res.status(500).json({
success: false,
error: 'Failed to process uploaded file'
})
}
})
module.exports = router9.3 Hotlink Protection and Security Configuration
javascript
/**
* CDN Hotlink Protection and Security Configuration Example
*/
// 1. Referer hotlink protection (prevent other websites from directly referencing your resources)
const refererConfig = {
// Whitelist mode: only allow the following Referers
allowList: [
'*.myapp.com', // Main site
'*.myapp.cn', // Domestic site
'localhost:*', // Local development
'127.0.0.1:*'
],
// Blacklist mode (optional): block the following Referers
blockList: [
'*.competitor.com', // Competitors
'spam-site.com'
],
// Empty Referer handling: whether to allow direct access (typing URL in browser address bar)
allowEmptyReferer: false // Recommended false for production, can be true for testing
}
// 2. URL authentication (more secure hotlink protection with timestamp and signature)
class URLAuth {
constructor(config) {
this.key = config.key // Authentication key, stored only on the server
this.expireTime = config.expireTime || 3600 // Default 1 hour validity
}
/**
* Generate an authenticated URL
* @param {string} url - Original URL, e.g., https://cdn.example.com/images/photo.jpg
* @param {number} expireIn - Validity period (seconds)
* @returns {string} URL with authentication parameters
*/
sign(url, expireIn = this.expireTime) {
const urlObj = new URL(url)
const pathname = urlObj.pathname
const timestamp = Math.floor(Date.now() / 1000) + expireIn
// Construct signature string (format varies by provider; this is a generic example)
const signStr = `${pathname}-${timestamp}-${this.key}`
const signature = this._md5(signStr)
// Add authentication parameters
urlObj.searchParams.set('sign', signature)
urlObj.searchParams.set('t', timestamp.toString())
return urlObj.toString()
}
/**
* Verify URL signature (used at CDN edge or origin)
*/
verify(url) {
const urlObj = new URL(url)
const signature = urlObj.searchParams.get('sign')
const timestamp = parseInt(urlObj.searchParams.get('t'))
const pathname = urlObj.pathname
// Check if expired
if (timestamp < Math.floor(Date.now() / 1000)) {
return { valid: false, error: 'URL expired' }
}
// Verify signature
const signStr = `${pathname}-${timestamp}-${this.key}`
const expectedSign = this._md5(signStr)
if (signature !== expectedSign) {
return { valid: false, error: 'Invalid signature' }
}
return { valid: true }
}
_md5(str) {
// In real projects, use crypto-js or another MD5 library
// This is a demonstration only
return require('crypto').createHash('md5').update(str).digest('hex')
}
}
// Usage example
const auth = new URLAuth({
key: 'your-secret-key-only-known-by-server',
expireTime: 3600 // 1 hour validity
})
// Server generates signed URL
const signedUrl = auth.sign(
'https://cdn.example.com/private/document.pdf',
7200
)
// Result: https://cdn.example.com/private/document.pdf?sign=xxxxx&t=1699123456
// CDN edge or origin verification
const result = auth.verify(signedUrl)
if (!result.valid) {
// Return 403 Forbidden
}
// 3. IP Blacklist/Whitelist
const ipConfig = {
// Only allow specific IPs (suitable for internal systems)
whiteList: [
'192.168.1.0/24', // Internal network segment
'10.0.0.0/8'
],
// Block specific IPs (ban attackers)
blackList: ['1.2.3.4', '5.6.7.8']
}
// 4. UA (User-Agent) Blacklist/Whitelist
const uaConfig = {
// Block crawlers/download tools
blackList: [
'Wget',
'curl',
'python-requests',
'Scrapy',
'AhrefsBot',
'SemrushBot'
],
// Only allow browser access (strict mode)
whiteList: [
'Mozilla/*', // Modern browsers
'AppleWebKit/*'
]
}