9. 实战代码模板
9.1 前端直传对象存储(JavaScript)
javascript
/**
* 对象存储直传工具类
* 支持:阿里云 OSS、腾讯云 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 // 获取临时凭证的函数
}
/**
* 获取 STS 临时凭证
*/
async fetchCredentials() {
// 向后端申请临时凭证
const credentials = await this.getCredentials()
return {
accessKeyId: credentials.accessKeyId,
accessKeySecret: credentials.accessKeySecret,
sessionToken: credentials.securityToken || credentials.sessionToken,
expiration: credentials.expiration
}
}
/**
* 生成上传签名(适用于前端计算签名)
*/
generateSignature(credentials, fileKey, fileType, options = {}) {
const timestamp = new Date().toISOString()
const date = timestamp.slice(0, 10).replace(/-/g, '')
// 不同厂商的签名算法略有差异
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')
}
}
/**
* 单文件上传(小文件 < 100MB)
*/
async upload(file, options = {}) {
const credentials = await this.fetchCredentials()
const fileKey = this._generateFileKey(file, options.directory)
const formData = new FormData()
// 构建表单字段(不同厂商字段名不同)
const formFields = this._buildFormFields(
credentials,
fileKey,
file.type,
options
)
Object.entries(formFields).forEach(([key, value]) => {
formData.append(key, value)
})
formData.append('file', file)
// 发送上传请求
const uploadUrl = this._getUploadUrl()
const response = await fetch(uploadUrl, {
method: 'POST',
body: formData,
// 如果上传大文件,可能需要设置更长的超时
signal: options.signal // 支持 AbortController 取消上传
})
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
}
}
/**
* 分片上传(大文件 > 100MB)
*/
async multipartUpload(file, options = {}) {
const partSize = options.partSize || 10 * 1024 * 1024 // 默认 10MB/片
const parallel = options.parallel || 3 // 默认 3 个并发
const credentials = await this.fetchCredentials()
const fileKey = this._generateFileKey(file, options.directory)
// 1. 初始化分片上传
const uploadId = await this._initMultipartUpload(
credentials,
fileKey,
file.type
)
// 2. 计算分片
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. 上传分片(带并发控制和断点续传)
const uploadedParts = []
const failedParts = []
// 支持断点续传:检查哪些分片已上传
if (options.resume) {
const existingParts = await this._listParts(
credentials,
fileKey,
uploadId
)
for (const part of existingParts) {
uploadedParts.push(part)
}
}
// 过滤出未上传的分片
const pendingParts = parts.filter(
(p) => !uploadedParts.some((up) => up.partNumber === p.number)
)
// 并发上传
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
}
}
// 使用 Promise.all 控制并发
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)
}
}
}
// 检查是否所有分片都上传成功
if (uploadedParts.length !== totalParts) {
throw new Error(
`Upload incomplete: ${uploadedParts.length}/${totalParts} parts uploaded`
)
}
// 4. 完成分片上传(合并分片)
await this._completeMultipartUpload(
credentials,
fileKey,
uploadId,
uploadedParts
)
return {
url: this._getFileUrl(fileKey),
key: fileKey,
size: file.size,
parts: totalParts
}
}
/**
* 生成文件存储路径
*/
_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
}
// ============ 各厂商特定方法 ============
_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}`
}
// 各厂商的签名、分片上传等方法...(根据实际需求实现)
_buildFormFields(credentials, fileKey, fileType, options) {
// 各厂商表单字段构建逻辑
// 这里需要根据具体厂商的文档实现
return {}
}
async _initMultipartUpload(credentials, fileKey, fileType) {
// 各厂商初始化分片上传逻辑
return 'upload-id'
}
async _uploadPart(credentials, fileKey, uploadId, part) {
// 各厂商分片上传逻辑
return 'etag'
}
async _completeMultipartUpload(credentials, fileKey, uploadId, parts) {
// 各厂商完成分片上传逻辑
}
async _listParts(credentials, fileKey, uploadId) {
// 各厂商列出已上传分片逻辑
return []
}
}
// 使用示例
const uploader = new DirectUploader({
provider: 'oss',
region: 'cn-beijing',
bucket: 'myapp-images-prod',
getCredentials: async () => {
// 向后端申请临时凭证
const res = await fetch('/api/upload/credentials')
return res.json()
}
})
// 小文件上传
async function uploadAvatar(file) {
try {
const result = await uploader.upload(file, {
directory: 'avatars',
onProgress: (progress) => {
console.log(`上传进度: ${progress.percent}%`)
}
})
console.log('上传成功:', result.url)
return result
} catch (error) {
console.error('上传失败:', error)
throw error
}
}
// 大文件分片上传
async function uploadVideo(file) {
try {
const result = await uploader.multipartUpload(file, {
directory: 'videos',
partSize: 10 * 1024 * 1024, // 10MB 每片
parallel: 3, // 3 个并发
resume: true, // 支持断点续传
onProgress: (progress) => {
console.log(
`上传进度: ${progress.percent}%, 已传 ${progress.loaded}/${progress.total}`
)
},
onPartComplete: (part) => {
console.log(`分片 ${part.number} 上传完成`)
}
})
console.log('上传成功:', result.url)
return result
} catch (error) {
console.error('上传失败:', error)
// 可以在这里实现重试逻辑或保存断点信息
throw error
}
}9.2 后端临时凭证服务(Node.js/Express)
javascript
/**
* 对象存储 STS 临时凭证服务
* 支持:阿里云 OSS、腾讯云 COS、AWS S3
*/
const express = require('express')
const STS = require('ali-oss').STS // 阿里云
// const COS = require('cos-nodejs-sdk-v5') // 腾讯云
const router = express.Router()
// 配置
const config = {
// 阿里云 OSS 配置
oss: {
accessKeyId: process.env.OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
region: 'oss-cn-beijing',
bucket: 'myapp-images-prod',
// STS 角色 ARN(需要在 RAM 控制台创建)
roleArn: process.env.OSS_STS_ROLE_ARN
}
}
/**
* 获取 STS 临时凭证(阿里云 OSS)
* POST /api/upload/credentials
*/
router.post('/credentials', async (req, res) => {
try {
// 1. 验证用户身份(根据实际情况实现)
const userId = req.user?.id
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' })
}
// 2. 生成唯一的文件路径前缀(用于权限隔离)
const date = new Date()
const prefix = `uploads/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${userId}/`
// 3. 创建 STS 客户端
const sts = new STS({
accessKeyId: config.oss.accessKeyId,
accessKeySecret: config.oss.accessKeySecret
})
// 4. 申请临时凭证
const result = await sts.assumeRole(
config.oss.roleArn,
{
// Policy 限制权限范围(最小权限原则)
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, // 凭证有效期 1 小时
'web-upload-session-' + Date.now()
)
// 5. 返回凭证和配置
res.json({
success: true,
data: {
// STS 临时凭证
credentials: {
accessKeyId: result.credentials.AccessKeyId,
accessKeySecret: result.credentials.AccessKeySecret,
sessionToken: result.credentials.SecurityToken,
expiration: result.credentials.Expiration
},
// 上传配置
config: {
provider: 'oss',
region: config.oss.region,
bucket: config.oss.bucket,
endpoint: `https://${config.oss.bucket}.${config.oss.region}.aliyuncs.com`,
prefix: prefix, // 文件路径前缀
// 安全限制
maxSize: 100 * 1024 * 1024, // 最大 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
})
}
})
/**
* 回调通知:前端上传完成后通知后端
* 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. 验证文件是否存在
// 2. 保存文件信息到数据库
const fileRecord = await db.files.create({
userId,
key,
etag,
size,
mimeType,
originalName,
url: `https://cdn.example.com/${key}`,
createdAt: new Date()
})
// 3. 异步处理:生成缩略图、提取元数据、内容审核等
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 防盗链与安全配置
javascript
/**
* CDN 防盗链与安全配置示例
*/
// 1. Referer 防盗链(防止其他网站直接引用你的资源)
const refererConfig = {
// 白名单模式:只允许以下 Referer 访问
allowList: [
'*.myapp.com', // 主站
'*.myapp.cn', // 国内站
'localhost:*', // 本地开发
'127.0.0.1:*'
],
// 黑名单模式(可选):禁止以下 Referer
blockList: [
'*. competitor.com', // 竞争对手
'spam-site.com'
],
// 空 Referer 处理:是否允许直接访问(浏览器地址栏输入 URL)
allowEmptyReferer: false // 生产环境建议 false,测试环境可 true
}
// 2. URL 鉴权(更安全的防盗链,带时间戳和签名)
class URLAuth {
constructor(config) {
this.key = config.key // 鉴权密钥,只在服务端保存
this.expireTime = config.expireTime || 3600 // 默认 1 小时有效期
}
/**
* 生成带鉴权的 URL
* @param {string} url - 原始 URL,如 https://cdn.example.com/images/photo.jpg
* @param {number} expireIn - 有效期(秒)
* @returns {string} 带鉴权参数的 URL
*/
sign(url, expireIn = this.expireTime) {
const urlObj = new URL(url)
const pathname = urlObj.pathname
const timestamp = Math.floor(Date.now() / 1000) + expireIn
// 构造签名字符串(不同厂商格式不同,这里是通用示例)
const signStr = `${pathname}-${timestamp}-${this.key}`
const signature = this._md5(signStr)
// 添加鉴权参数
urlObj.searchParams.set('sign', signature)
urlObj.searchParams.set('t', timestamp.toString())
return urlObj.toString()
}
/**
* 验证 URL 签名(在 CDN 边缘或源站使用)
*/
verify(url) {
const urlObj = new URL(url)
const signature = urlObj.searchParams.get('sign')
const timestamp = parseInt(urlObj.searchParams.get('t'))
const pathname = urlObj.pathname
// 检查是否过期
if (timestamp < Math.floor(Date.now() / 1000)) {
return { valid: false, error: 'URL expired' }
}
// 验证签名
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) {
// 实际项目中使用 crypto-js 或其他 MD5 库
// 这里仅作示例
return require('crypto').createHash('md5').update(str).digest('hex')
}
}
// 使用示例
const auth = new URLAuth({
key: 'your-secret-key-only-known-by-server',
expireTime: 3600 // 1 小时有效期
})
// 服务端生成带签名的 URL
const signedUrl = auth.sign(
'https://cdn.example.com/private/document.pdf',
7200
)
// 结果:https://cdn.example.com/private/document.pdf?sign=xxxxx&t=1699123456
// CDN 边缘或源站验证
const result = auth.verify(signedUrl)
if (!result.valid) {
// 返回 403 Forbidden
}
// 3. IP 黑白名单
const ipConfig = {
// 只允许特定 IP 访问(适合内部系统)
whiteList: [
'192.168.1.0/24', // 内网网段
'10.0.0.0/8'
],
// 禁止特定 IP 访问(封禁攻击者)
blackList: ['1.2.3.4', '5.6.7.8']
}
// 4. UA(User-Agent)黑白名单
const uaConfig = {
// 禁止爬虫/下载工具
blackList: [
'Wget',
'curl',
'python-requests',
'Scrapy',
'AhrefsBot',
'SemrushBot'
],
// 只允许浏览器访问(严格模式)
whiteList: [
'Mozilla/*', // 现代浏览器
'AppleWebKit/*'
]
}