Yasin

Yasin

OSS 对象存储:是什么、为什么、怎么用

引言

在现代 Web 应用中,用户头像、商品图片、视频文件、导出报表……这些非结构化数据如果直接放到服务器磁盘上,既占用宝贵的计算资源,又难以横向扩展。对象存储(Object Storage Service,OSS) 正是为解决这一问题而生的云存储方案。

本文以阿里云 OSS 为主线(AWS S3 API 设计高度相似,思路完全通用),系统讲解 OSS 的核心概念、前后端集成方式、权限控制和最佳实践。

核心概念

什么是对象存储

对象存储是一种以「对象(Object)」为基本单元的存储架构。每个对象由三部分组成:

部分 说明
Data(数据) 文件的实际二进制内容
Key(键) 对象的唯一标识符,类似文件路径,如 avatars/user-123.jpg
Metadata(元数据) 描述对象的信息,如 Content-Type、自定义标签、上传时间

与传统文件系统(有目录树)不同,对象存储是扁平的键值空间——「目录」只是 Key 的前缀,并不真实存在。这使得它可以无限水平扩展。

核心术语

  • Bucket(存储桶):对象的容器,类似一个独立的命名空间。一个账号可以创建多个 Bucket,不同业务用不同 Bucket 隔离。
  • Object(对象):存储的基本单元,即一个文件 + 其元数据。
  • Endpoint(访问域名):访问 OSS 的域名,分内网(ECS 内部访问免流量费)和外网两种。
  • AccessKey:用于 API 鉴权的密钥对(AccessKeyId + AccessKeySecret),相当于账号密码,绝对不能泄露到前端代码或 Git 仓库
  • STS(Security Token Service):临时安全令牌服务,用于给前端发放有时效的上传凭证,是前端直传的核心机制。
  • 预签名 URL(Pre-signed URL):携带签名信息的临时访问链接,可在不暴露 AccessKey 的前提下授权用户下载私有文件。

对象存储 vs 文件系统 vs 数据库 BLOB

维度 对象存储 文件系统(NAS) 数据库 BLOB
扩展性 无限水平扩展 有限,需挂载 受数据库性能限制
成本 极低(按量付费) 中等
访问方式 HTTP API / SDK POSIX(文件路径) SQL
CDN 集成 原生支持 复杂 不适合
适合场景 媒体、文档、备份 共享代码、配置 极小文件

实际应用

场景一:后端服务器上传(Node.js)

最简单的模式:前端将文件传给后端,后端转存到 OSS。适合对文件有处理需求的场景(压缩、格式转换、病毒扫描)。

npm install ali-oss
// server/oss.js
const OSS = require('ali-oss')

const client = new OSS({
  region: 'oss-cn-hangzhou',
  accessKeyId: process.env.OSS_ACCESS_KEY_ID,       // 从环境变量读取,绝不硬编码
  accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
  bucket: 'my-app-bucket',
})

// 上传 Buffer 或本地文件路径
async function uploadFile(key, filePath) {
  const result = await client.put(key, filePath)
  console.log('上传成功:', result.url)
  return result.url
}

// 上传流(适合管道处理大文件)
async function uploadStream(key, readableStream) {
  const result = await client.putStream(key, readableStream)
  return result.url
}

// 生成私有文件的预签名下载链接(1 小时有效)
function getSignedUrl(key) {
  return client.signatureUrl(key, { expires: 3600 })
}

在 Express 中接收并转存:

const multer = require('multer')
const upload = multer({ storage: multer.memoryStorage() })

app.post('/api/upload', upload.single('file'), async (req, res) => {
  const { originalname, buffer, mimetype } = req.file
  const key = `uploads/${Date.now()}-${originalname}`

  const result = await client.put(key, buffer, {
    headers: { 'Content-Type': mimetype },
  })

  res.json({ url: result.url, key })
})

场景二:前端直传(推荐方案)

让文件绕过服务器直接上传到 OSS,减少服务器带宽压力。关键:由服务端签发 STS 临时凭证,而非暴露 AccessKey。

流程:

  1. 前端请求后端获取 STS 临时凭证
  2. 后端调用 STS 服务,生成带有限权限的临时 Token(有效期 15 分钟~1 小时)
  3. 前端用临时凭证直接上传到 OSS
// server/sts.js - 签发临时凭证
const STS = require('@alicloud/sts-sdk')  // 或使用 ali-oss 内置的 STS

app.get('/api/oss-token', async (req, res) => {
  const sts = new STS({
    accessKeyId: process.env.OSS_ACCESS_KEY_ID,
    accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
  })

  // 限制临时凭证只能上传到指定目录
  const policy = {
    Version: '1',
    Statement: [{
      Effect: 'Allow',
      Action: ['oss:PutObject'],
      Resource: [`acs:oss:*:*:my-app-bucket/user-uploads/${req.user.id}/*`],
    }],
  }

  const result = await sts.assumeRole(
    'acs:ram::xxxxx:role/oss-upload-role',
    JSON.stringify(policy),
    3600,  // 有效期 1 小时
    'session-name'
  )

  res.json({
    accessKeyId: result.Credentials.AccessKeyId,
    accessKeySecret: result.Credentials.AccessKeySecret,
    stsToken: result.Credentials.SecurityToken,
    expiration: result.Credentials.Expiration,
  })
})
// client/upload.js - 前端直传
import OSS from 'ali-oss'

async function uploadToOSS(file) {
  // 1. 从后端获取临时凭证
  const { accessKeyId, accessKeySecret, stsToken } = await fetch('/api/oss-token').then(r => r.json())

  // 2. 创建客户端(使用临时凭证)
  const client = new OSS({
    region: 'oss-cn-hangzhou',
    accessKeyId,
    accessKeySecret,
    stsToken,
    bucket: 'my-app-bucket',
  })

  // 3. 直传文件
  const key = `user-uploads/${userId}/${Date.now()}-${file.name}`
  const result = await client.put(key, file)

  return result.url
}

场景三:服务端签名 + 表单直传(PostObject)

适合 H5 / 小程序场景,前端完全不引入 OSS SDK,改用原生 FormData:

// server - 生成 PostObject 签名
const crypto = require('crypto')

app.get('/api/oss-policy', (req, res) => {
  const expiration = new Date(Date.now() + 60 * 60 * 1000).toISOString() // 1小时后过期
  const dir = `user-uploads/${req.user.id}/`

  const policy = Buffer.from(JSON.stringify({
    expiration,
    conditions: [
      ['content-length-range', 0, 10 * 1024 * 1024], // 最大 10MB
      ['starts-with', '$key', dir],                   // 只能上传到指定目录
    ],
  })).toString('base64')

  const signature = crypto
    .createHmac('sha1', process.env.OSS_ACCESS_KEY_SECRET)
    .update(policy)
    .digest('base64')

  res.json({
    host: 'https://my-app-bucket.oss-cn-hangzhou.aliyuncs.com',
    OSSAccessKeyId: process.env.OSS_ACCESS_KEY_ID,
    policy,
    signature,
    dir,
  })
})
// client - 表单直传(无需 SDK)
async function uploadWithPolicy(file) {
  const { host, OSSAccessKeyId, policy, signature, dir } = await fetch('/api/oss-policy').then(r => r.json())

  const key = `${dir}${Date.now()}-${file.name}`
  const formData = new FormData()
  formData.append('key', key)
  formData.append('OSSAccessKeyId', OSSAccessKeyId)
  formData.append('policy', policy)
  formData.append('Signature', signature)
  formData.append('file', file)  // file 必须是最后一个字段!

  await fetch(host, { method: 'POST', body: formData })
  return `${host}/${key}`
}

常见误区

1. 把 AccessKey 写进前端代码

// ❌ 极度危险!AccessKey 会暴露在 JS Bundle 中
const client = new OSS({
  accessKeyId: 'LTAI5xxxxx',
  accessKeySecret: 'xxxxxx',  // 硬编码,任何人都能从网络请求中看到
})

正确做法:使用 STS 临时凭证,或 PostObject 签名,AccessKey 永远只留在服务端。

2. Bucket 公开读 + 存储敏感文件

公开读的 Bucket 意味着任何知道 URL 的人都能访问文件。合同、身份证、医疗记录等敏感数据必须使用私有 Bucket + 预签名 URL

3. 忽略 CORS 配置

前端直传时必须在 OSS 控制台配置 CORS 规则,允许来自你域名的跨域请求,否则浏览器会报 CORS 错误。

4. 大文件不用分片上传

超过 100MB 的文件应使用分片上传(Multipart Upload),避免网络中断导致整个上传失败:

// ali-oss 内置分片上传,自动处理断点续传
const result = await client.multipartUpload(key, file, {
  progress: (percent) => console.log(`上传进度: ${(percent * 100).toFixed(1)}%`),
  partSize: 5 * 1024 * 1024, // 每片 5MB(最小值)
})

企业实践与业界方案

阿里云 OSS 在电商场景的典型架构

业界常见做法:

用户端
  ├─ 获取 STS 凭证 ──► 应用服务器(生成临时凭证)
  ├─ 直传文件 ──────► OSS Bucket(私有读)
  │                         │
  └─ 访问资源 ──────► CDN(绑定自定义域名)──► 回源 OSS

CDN 缓存命中率高,OSS 流量费用大幅降低,同时用户访问延迟也更低。图片还可以通过 OSS 图片处理(Image Processing)URL 参数实现实时裁剪、缩放、格式转换(如 WebP):

https://example.com/product/img.jpg?x-oss-process=image/resize,w_800/format,webp

AWS S3 + CloudFront 方案

AWS 的对应方案是 S3(存储)+ CloudFront(CDN)。S3 的 API 设计是业界事实标准,阿里云 OSS、MinIO、Cloudflare R2 等都兼容 S3 协议,迁移成本极低。

私有化部署:MinIO

业界常见做法:不方便使用公有云时(金融、政务行业),用 MinIO 在私有机房搭建兼容 S3 协议的对象存储服务,代码几乎不需要修改即可切换。

关联知识

前置知识:

  • HTTP 协议基础(PUT / POST 请求、Content-Type、CORS)
  • HMAC-SHA1 签名原理(理解预签名 URL 的安全机制)
  • IAM / RAM 权限控制模型(理解 STS 临时凭证)

延伸知识:

  • OSS 生命周期规则(自动归档/删除过期文件,降低存储成本)
  • OSS 事件通知(文件上传后触发 Serverless 函数进行图片处理)
  • CDN 缓存策略与 Cache-Control Header 的配合
  • AWS S3 Transfer Acceleration(加速全球上传)
  • Cloudflare R2(零出口流量费,S3 兼容)

参考文档:

实践建议

  1. 本地用 MinIO 替代云 OSS 做开发调试,避免产生费用,且与生产环境 API 完全一致:

    docker run -p 9000:9000 -p 9001:9001 \
      -e MINIO_ROOT_USER=minioadmin \
      -e MINIO_ROOT_PASSWORD=minioadmin \
      minio/minio server /data --console-address ":9001"
    

    访问 http://localhost:9001 可进入 MinIO 控制台创建 Bucket。

  2. 用环境变量管理密钥,配合 .env 文件(加入 .gitignore),永远不硬编码:

    # .env
    OSS_ACCESS_KEY_ID=LTAI5xxxxx
    OSS_ACCESS_KEY_SECRET=xxxxx
    OSS_BUCKET=my-app-bucket
    OSS_REGION=oss-cn-hangzhou
    
  3. 推荐练习:实现一个头像上传功能,包含:服务端签发 STS 凭证 → 前端直传 → CDN URL 回显,完整走通整个链路。

总结

OSS(对象存储)是现代应用存储非结构化文件的标准方案,核心是以 HTTP API 访问「Bucket + Object」的键值空间;安全核心是永远不把 AccessKey 暴露给客户端,而是通过 STS 临时凭证或 PostObject 签名实现前端安全直传;工程核心是将 OSS 与 CDN 结合,用图片处理、生命周期规则降低成本、提升性能。