Files
2026-05-24 11:42:38 +08:00

683 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [后端, Go, 对象存储, S3, 文件上传]
create time: 2026-05-07 19:12
---
# OSS 对象存储
## 概述
OSS(Object Storage Service)即**对象存储**,是一种将非结构化数据作为对象存储在云端的方案。阿里云的 OSS、AWS 的 S3 以及兼容 S3 协议的 MinIO 本质上是同一种技术——通过 HTTP API 对海量二进制数据进行增删改查。理解其核心概念和工作模式,是构建现代 Web 应用基础设施的关键一环。
思考题:和传统的关系型数据库相比,为什么图片、视频、日志文件这类数据不适合存在 MySQL 里?
## 正文
### 1. 核心概念
对象存储有三个基本概念:
| 概念 | 说明 | 类比关系型数据库 |
|------|------|------------------|
| **Bucket(存储桶)** | 对象的容器,类似"数据库" | Database / Table |
| **Object(对象)** | 实际存储的数据,包含 Key + Body + Metadata,类似"行记录" | Row |
| **Key(键)** | 对象的全局唯一标识,类似文件系统路径 | File Path |
> **关键点**:对象存储是一个**扁平结构**——没有真正的目录层级,所谓的 `folder/` 只是 Key 中的一部分字符(如 `photos/2026/vacation.jpg`)。但管理面板会按 `/` 做可视化分组。
```mermaid
flowchart LR
subgraph OSS_BUCKET["Bucket: my-app-data"]
direction TB
O1["Key: uploads/avatar/001.png<br/>Body: (PNG binary data)"]
O2["Key: docs/report.pdf<br/>Body: (PDF binary data)"]
O3["Key: backups/db-dump.sql.gz<br/>Body: (SQL compressed)"]
end
classDef bucketStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
class OSS_BUCKET bucketStyle
```
**Region 和 Endpoint:**
每个 Bucket 必须属于一个地域(Region),Region 决定了数据的物理存储位置。访问时需要使用对应的 Endpoint:
| 地域 | 内网 Endpoint | 外网 Endpoint |
|------|---------------|---------------|
| 杭州 (oss-cn-hangzhou) | `oss-cn-hangzhou-internal.aliyuncs.com` | `oss-cn-hangzhou.aliyuncs.com` |
| 上海 (oss-cn-shanghai) | `oss-cn-shanghai-internal.aliyuncs.com` | `oss-cn-shanghai.aliyuncs.com` |
| 北京 (oss-cn-beijing) | `oss-cn-beijing-internal.aliyuncs.com` | `oss-cn-beijing.aliyuncs.com` |
> **最佳实践**:当你的 Go 服务和 OSS Bucket 在同一 Region 时,务必使用**内网 Endpoint**。内网流量免费且速度比外网快 5~10 倍。
### 2. Go SDK 选型
目前 Go 生态中有两套主流 OSS/S3 SDK:
```mermaid
graph TD
A["Go 对象存储 SDK"] --> B["Aliyun OSS SDK<br/>alibabacloud-oss-go-sdk-v2"]
A --> C["Minio Client SDK<br/>minio/go-minio"]
A --> D["AWS SDK for Go v2<br/>aws/aws-sdk-go-v2/s3"]
B --> B1["适用于阿里云 OSS"]
B --> B2["API 完全覆盖 OSS 功能"]
B --> B3["绑定阿里云鉴权体系"]
C --> C1["兼容 S3/OSS/COS/GCS 等"]
C --> C2["API 接近标准库 io.Reader"]
C --> C3["推荐用于多云/混合云场景"]
D --> D1["AWS 官方 SDK"]
D --> D2["功能最全面"]
D --> D3["体积大, 编译慢"]
style C1 fill:#e8f5e9,stroke:#2e7d32
style B1 fill:#fff3e0,stroke:#f57c00
style D1 fill:#e3f2fd,stroke:#1565c0
```
| 对比项 | Aliyun OSS SDK | Minio Client | AWS S3 SDK v2 |
|--------|---------------|-------------|---------------|
| 适配服务 | 仅阿里云 OSS | S3/OSS/COS/GCS等 | AWS S3 / Cloudflare R2 |
| 安装大小 | ~5MB | ~2MB | ~150MB |
| API 风格 | RESTful 包装 | `io.Reader` 流式 | 深度异步链式 |
| 预签名 URL | ✅ 原生支持 | ✅ `PresignedGetObject` | ✅ `PresignObjectAPI` |
| 推荐度 | 只用阿里云 | **多云通用首选** | AWS 重度用户 |
> **结论**:如果你只做阿里云,用官方 SDK;如果需要同时对接 S3、MinIO 或其他兼容 S3 的服务,选择 Minio Client SDK。
### 3. 初始化客户端
**方式一:阿里云官方 SDK(v2)**
```go
import (
oss "github.com/alibabacloud-go/oss-20190517/v2/client"
)
// 从环境变量读取凭证,避免硬编码
client := oss.NewClient(&oss.Config{
Endpoint: oss.String("oss-cn-hangzhou.aliyuncs.com"),
AccessKeyId: oss.String(os.Getenv("OSS_ACCESS_KEY_ID")),
AccessKeySecret: oss.String(os.Getenv("OSS_ACCESS_KEY_SECRET")),
})
```
**方式二:Minio Client(更简洁,跨云兼容)**
```go
import "github.com/minio/minio-go/v7"
client, err := minio.New("oss-cn-hangzhou.aliyuncs.com", &minio.Options{
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
Secure: true, // HTTPS
})
if err != nil {
log.Fatal(err)
}
```
> **安全提醒**:永远不要将 AccessKey 写在代码里。生产环境应使用环境变量、密钥管理服务(KMS / Vault / 云厂商 IAM Role)注入。
### 4. Bucket 操作
#### 创建与查询
```go
// 创建 Bucket(Minio 写法)
bucketName := "my-app-uploads"
location := "cn-hangzhou"
err := client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{
Region: location,
})
// 如果已存在则 ErrBucketExists 忽略即可
// 检查 Bucket 是否存在
exists, _ := client.BucketExists(ctx, bucketName)
```
#### 权限模型
| ACL | 说明 | 适用场景 |
|-----|------|---------|
| `Private`(默认) | 只有 Owner 可读写 | 用户上传的文件、备份 |
| `PublicRead` | Owner 读写 + 所有人可读 | CDN 加速的图片资源 |
| `PublicReadWrite` | 所有人可读写 | **极不推荐** — 可能被恶意刷费 |
### 5. 文件上传:三种架构对比
前端要上传图片/文件到 OSS,主要有以下三种架构方案。选择合适的方案直接影响系统的**可扩展性**、**安全性**和**开发成本**。
#### 方案 A:服务端中转上传(Server Relay)
```mermaid
sequenceDiagram
participant FE as 浏览器 / 客户端
participant GO as Gin Server
participant OSS as OSS 服务
FE->>GO: POST 文件 (multipart/form-data)
Note over GO: 解析请求体 (io.Reader 流式)
GO->>OSS: PUT 上传 (Stream 转发)
OSS-->>GO: 200 OK (ETag, Location)
GO-->>FE: 200 OK (返回 OSS 访问地址)
```
**工作流程:**
1. 客户端将文件 POST 到 Go 服务端(multipart/form-data)
2. Go 服务端接收后,通过 SDK 将文件写入 OSS
3. 返回文件的 OSS 访问地址给客户端
```go
func uploadWithRelay(c *gin.Context) {
// 限制文件大小(防内存溢出 + 防恶意大文件)
c.Request.ParseMultipartForm(32 << 20) // 最大 32MB
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "invalid file"})
return
}
defer file.Close()
// 校验文件类型
buf := make([]byte, 512)
file.Read(buf)
contentType := http.DetectContentType(buf)
allowed := map[string]bool{
"image/jpeg": true, "image/png": true, "image/webp": true,
}
if !allowed[contentType] {
c.JSON(400, gin.H{"error": "unsupported file type"})
return
}
// rewind for reading again
file.Seek(0, io.SeekStart)
// 重命名防止冲突(UUID_原文件名)
fileName := uuid.New().String() + "_" + header.Filename
_, err = client.PutObject(c.Request.Context(), bucketName, fileName, file, -1, minio.PutObjectOptions{
ContentType: contentType,
})
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
// 记录元信息到数据库
db.Create(&FileRecord{Key: fileName, Size: header.Size, Type: contentType})
c.JSON(200, gin.H{
"url": fmt.Sprintf("https://%s.%s/%s", bucketName, endpoint, fileName),
"size": header.Size,
})
}
```
#### 方案 B:预签名 URL 直传(Pre-Signed URL)
```mermaid
sequenceDiagram
participant FE as 浏览器 / 客户端
participant GO as Gin Server
participant OSS as OSS 服务
FE->>GO: GET /upload/url?filename=photo.jpg
Note over GO: 验证登录态和权限
GO->>OSS: PresignedPutObject(photo.jpg, 15min)
OSS-->>GO: (预签名URL)含签名和过期参数
GO-->>FE: (返回URL和类型信息)
FE->>OSS: PUT 直接上传<br/>Content-Type: image/jpeg<br/>文件二进制数据
OSS-->>FE: 200 OK
opt 通知服务端完成
FE->>GO: POST /upload/callback<br/>文件名和元信息
Note over GO: 验证对象存在于 OSS 中
GO-->>FE: 200 OK (记录元信息到 DB)
end
```
**工作流程:**
1. 客户端请求服务端获取预签名 URL
2. 服务端调用 OSS 的 `PresignedPutObject` 生成有时效性的上传链接
3. 客户端拿到 URL 后**直接 PUT 到 OSS**,不经服务端
4. 上传完成后回调服务端,记录元信息到数据库
```go
// --- 服务端:生成预签名 URL ---
func getPresignedUploadURL(c *gin.Context) {
filename := c.Query("filename")
contentType := c.DefaultQuery("contentType", "application/octet-stream")
url, err := client.PresignedPutObject(
context.Background(),
bucketName,
filepath.Join("uploads", filename),
15*time.Minute,
)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"uploadURL": url.String(),
"fileName": filepath.Join("uploads", filename),
"contentType": contentType,
"expiresIn": 900, // 秒
})
}
// --- 客户端:用返回的 URL 直接 PUT 到 OSS ---
/*
const resp = await fetch('/api/upload/url?filename=avatar.jpg');
const { uploadURL, contentType } = await resp.json();
await fetch(uploadURL, {
method: 'PUT',
headers: { 'Content-Type': contentType },
body: formData.get('file'),
});
// 上传完成后通知服务端
await fetch('/api/upload/callback', {
method: 'POST',
body: JSON.stringify({ fileName: 'avatar.jpg', fileSize: 102400 }),
});
*/
```
> **详细说明**:预签名 URL 的完整流程和安全要点请参考第 9 节。
#### 方案 C:STS 临时凭证直传(STS Credentials)
```mermaid
sequenceDiagram
participant FE as 浏览器 / 客户端
participant GO as Gin Server
participant STS as STS 服务
participant OSS as OSS 服务
FE->>GO: 请求上传权限
Note over GO: 验证用户身份
GO->>STS: AssumeRole (角色Arn + SessionName)
STS-->>GO: AccessKeyId + AccessKeySecret + SecurityToken<br/>有效期15分钟
GO-->>FE: (返回临时凭证)
FE->>OSS: 用临时凭证直接上传<br/>PostObject API
OSS-->>FE: 200 OK
```
**工作流程:**
1. 客户端请求服务端获取上传权限
2. 服务端调用 STS(Security Token Service)AssumeRole 获取临时凭据
3. 将临时凭据(AccessKeyId + Secret + Token)发给客户端
4. 客户端直接使用这些凭据通过 OSS PostObject API 上传
```go
import "github.com/aliyun/aliyun-sts-go-sdk"
func getSTSCredentials(c *gin.Context) {
stsClient := stssdk.NewClientWithAccessKey("cn-hangzhou", masterAK, masterSK)
request := stssdk.CreateAssumeRoleRequest()
request.RoleArn = "acs:ram::<YOUR_ACCOUNT_ID>:role/oss-upload-role"
request.RoleSessionName = "web-session-" + userID
request.DurationSeconds = 900 // 15 分钟
request.Policy = `{
"Statement": [{
"Action": ["oss:PutObject", "oss:PostObject"],
"Effect": "Allow",
"Resource": ["acs:oss:*:*:my-bucket/uploads/*"]
}]
}`
response, err := stsClient.AssumeRole(request)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"accessKeyId": response.Credentials.AccessKeyId,
"accessKeySecret": response.Credentials.AccessKeySecret,
"securityToken": response.Credentials.SecurityToken,
"expiration": response.Credentials.Expiration,
})
}
```
> **关键区别**:预签名 URL 是一次性的、针对特定文件的上传链接;STS 临时凭证是一组通用的上传凭据,可以在有效期内用于上传任意文件(受 Policy 约束)。
#### 三种方案对比
| 维度 | 方案 A:服务端中转 | 方案 B:预签名 URL 直传 | 方案 C:STS 临时凭证 |
|------|-------------------|------------------------|--------------------|
| **流量走向** | 客户端 → 服务端 → OSS | 客户端 → OSS | 客户端 → OSS |
| **服务端带宽消耗** | N × 文件大小 ⚠️ | 零带宽 ✅ | 零带宽 ✅ |
| **水平扩展性** | 差(服务端成为瓶颈) | 优秀 | 优秀 |
| **延迟** | 往返两次(双 RTT) | 一次直传(单 RTT) | 一次直传(单 RTT) |
| **控制粒度** | 粗(整个文件过服务端) | 中(URL 级控制) | 细(Policy 级可控) |
| **实现复杂度** | 低 ✅ | 中 ⚡ | 高 ❌ |
| **安全性** | 高(服务端全拦截) | 中高(签名+过期) | 中(需配置好 Policy) |
| **适合场景** | 小文件 (<1MB)、需要服务端二次处理 | 普通上传(头像、截图等) | 大文件、批量上传、企业级应用 |
#### 如何选型?
```mermaid
flowchart TD
A["用户上传文件"] --> B{"文件大小?"}
B -- "<= 1MB" --> C{"是否需要服务端<br/>内容分析/转码?"}
B -- "> 1MB 或不确定" --> D{"预期并发量?"}
C -- "否" --> E["✅ 方案 B: 预签名 URL<br/>最简单+零带宽开销"]
C -- "是" --> F["✅ 方案 A: 服务端中转<br/>可控但限并发"]
D -- "QPS < 100" --> G["✅ 方案 B: 预签名 URL"]
D -- "QPS >= 100 或<br/>大批量/大文件" --> H["✅ 方案 C: STS 临时凭证<br/>灵活且可扩展"]
```
> **经验法则**:大多数 Web 应用选**方案 B(预签名 URL)**就足够了。只有在需要对文件内容进行服务端分析(如病毒扫描、图像压缩、OCR),或者并发量极大时才考虑方案 A 或 C。
---
*本节的代码示例即为各方案的完整实现;更多细节和上下文请参考:服务端中转 → 第 6 节(对象基本操作)、预签名 URL → 第 7 节、STS 临时凭证 → 第 9 节。*
### 6. 对象基本操作
#### 上传(服务端直传示例)
```go
// 小文件:< 100MB 时直接用 FPutObject
err := client.FPutObject(ctx, bucketName, "uploads/photo.jpg", "./photo.jpg", minio.PutObjectOptions{
ContentType: "image/jpeg",
})
// 大文件:自动分片上传(Minio SDK 内部处理)
file, _ := os.Open("large-video.mp4")
_, err = client.PutObject(ctx, bucketName, "videos/large.mp4", file, -1, minio.PutObjectOptions{
PartSize: 10 << 20, // 每片 10MB
ContentType: "video/mp4",
})
```
> **分片阈值**:Minio SDK 默认当文件超过 **5MB** 时自动启动分片上传,最大支持单文件 **50TB**。无需手动实现。
#### 下载
```go
// 完整下载 → io.Reader
obj, err := client.GetObject(ctx, bucketName, "uploads/photo.jpg", minio.GetObjectOptions{})
defer obj.Close()
data, _ := io.ReadAll(obj)
_ = data // 写入数据库或直接返回给前端
// 范围下载(只读文件的某个区间)
opts := minio.GetObjectOptions{}
opts.Set("Range", "bytes=0-1023") // 只读前 1KB
```
#### 删除
```go
// 删除单个对象
client.RemoveObject(ctx, bucketName, "uploads/old-photo.jpg", minio.RemoveObjectOptions{})
// 批量删除
objsCh := make(chan minio.ObjectInfo, 100)
for info := range objsCh {
client.RemoveObject(ctx, bucketName, info.Key, minio.RemoveObjectOptions{})
}
close(objsCh)
client.RemoveObjects(ctx, bucketName, objsCh, minio.RemoveObjectsOptions{})
```
#### 列出对象
```go
// 列出以 "uploads/" 开头的对象(模拟目录效果)
objects := client.ListObjects(ctx, bucketName, minio.ListObjectsOptions{
Prefix: "uploads/", // 前缀过滤
Recursive: true, // false 则只在虚拟"目录"层面
})
for obj := range objects {
fmt.Printf("%-40s %8d bytes %s\n", obj.Key, obj.Size, obj.LastModified)
}
```
### 7. 预签名 URL — 免服务端中转
**这是对象存储最常用的高级模式之一。**
> **核心问题**:用户上传图片时,如果不走对象存储直传,而是先 POST 到 Go 服务端再由服务端转发到 OSS——当 1000 个用户同时上传 5MB 图片时,Go 进程需要消耗多少带宽?
答案很简单:**5MB × 1000 = 5GB 内存 + 带宽压力**。这就是为什么应该让客户端直接跟 OSS 对话。
**预签名 URL 流程:**
```mermaid
sequenceDiagram
participant FE as 浏览器 / 客户端
participant GO as Gin Server
participant OSS as OSS 服务
FE->>GO: GET /upload/url<br/>?filename=photo.jpg
Note over GO: 验证用户登录态和权限
GO->>OSS: PresignedGetObject(photo.jpg, 15min)
OSS-->>GO: (预签名URL)<br/>含签名和过期参数
GO-->>FE: (返回URL和类型信息)
FE->>OSS: PUT直接上传<br/>Content-Type: image/jpeg<br/>文件二进制数据
OSS-->>FE: 200 OK
opt 通知服务端完成
FE->>GO: POST /upload/callback<br/>文件名和元信息
GO-->>FE: 200 OK
end
```
> **安全要点**:预签名 URL 有时间限制(通常 5~15 分钟),过期后无效。即使 URL 泄露也无法长期滥用。
**代码示例:**
```go
func getPresignedUploadURL(c *gin.Context) {
filename := c.Query("filename")
contentType := c.DefaultQuery("contentType", "application/octet-stream")
// 生成一个有效期 15 分钟的 PUT 预签名 URL
// 注意:PresignedPutObject 不允许设置 Content-Type 约束
// 如需严格校验,应在 Callback 中二次确认
url, err := client.PresignedPutObject(
context.Background(),
bucketName,
filepath.Join("uploads", filename),
15*time.Minute,
)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"uploadURL": url.String(),
"fileName": filepath.Join("uploads", filename),
"contentType": contentType,
"expiresIn": 900, // 秒
})
}
```
**POST 端回调验证(防越权上传):**
```go
func uploadCallback(c *gin.Context) {
var req struct {
FileName string `json:"fileName" binding:"required"`
FileSize int64 `json:"fileSize" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "invalid params"})
return
}
key := filepath.Join("uploads", req.FileName)
// 检查对象是否真的存在于 OSS 中
_, err := client.StatObject(context.Background(), bucketName, key, minio.StatObjectOptions{})
if err != nil {
c.JSON(404, gin.H{"error": "file not found in OSS"})
return
}
// 保存元信息到数据库
db.Create(&FileRecord{Key: key, Size: req.FileSize})
c.JSON(200, gin.H{"status": "recorded"})
}
```
### 8. 生命周期管理(Lifecycle)
频繁删除和上传会产生大量碎片化的旧版本,合理配置 Lifecycle 可以节省成本并避免磁盘爆炸:
```go
import "github.com/minio/minio-go/v7/pkg/lifecycle"
err := client.SetBucketLifecycle(ctx, bucketName, &lifecycle.Configuration{
Rules: []lifecycle.Rule{
{
ID: "expire-old-uploads",
Status: lifecycle.RuleEnabled,
Prefix: "uploads/",
Expiration: lifecycle.Expiration{Days: 90}, // 90天后自动删除
},
{
ID: "transition-rare-access",
Status: lifecycle.RuleEnabled,
Prefix: "backups/",
Transition: lifecycle.Transition{
Days: 30,
StorageClass: "GLACIER", // 转归档存储(S3 兼容写法)
},
},
},
})
```
**存储类型与成本对比(阿里云为例):**
| 存储类型 | 单价 (元/GB/月) | 取回费用 | 最低存储时长 | 适用场景 |
|---------|---------------|---------|------------|---------|
| 标准存储(Standard) | ~0.12 | 无 | 无 | 活跃数据、频繁访问 |
| 低频访问(IA) | ~0.084 | 有 | 30天 | 很少读取但需即时访问 |
| 归档存储 | ~0.042 | 有 + 恢复时间 | 60天 | 合规存档、历史备份 |
| 冷归档 | ~0.021 | 有 + 数小时恢复 | 180天 | 极低频、合规留存 |
> **策略建议**:新创建的 Bucket 使用标准存储即可。在业务逻辑中标记哪些文件属于"冷数据",然后通过 Lifecycle 规则自动降级。不要人为在每个上传接口中判断冷热。
### 9. 并发与安全注意事项
**常见陷阱和解决方案:**
```mermaid
flowchart TD
subgraph TRAPS["常见陷阱"]
T1["AccessKey泄露<br/>导致账单爆炸"]
T2["未限流<br/>OSS请求被拒绝"]
T3["命名冲突<br/>同名文件覆盖"]
T4["内存溢出<br/>全量读入内存"]
T5["内网跨地域<br/>用了公网Endpoint"]
end
subgraph FIXES["解决方案"]
F1["使用RAM子账号+STS<br/>临时凭证(15min过期)"]
F2["客户端侧限速+重试退避<br/>指数退避算法"]
F3["使用UUID或NanoID重命名<br/>fileUUID_扩展名"]
F4["始终用io.Reader流式处理<br/>避免ReadAll读大文件"]
F5["确保ECS与Bucket同Region<br/>使用内网internal endpoint"]
end
T1 --> F1
T2 --> F2
T3 --> F3
T4 --> F4
T5 --> F5
style T1 fill:#ffebee,stroke:#c62828
style T2 fill:#ffebee,stroke:#c62828
style T3 fill:#fff3e0,stroke:#f57c00
style T4 fill:#ffebee,stroke:#c62828
style T5 fill:#fff3e0,stroke:#f57c00
style F1 fill:#e8f5e9,stroke:#2e7d32
style F2 fill:#e8f5e9,stroke:#2e7d32
style F3 fill:#e8f5e9,stroke:#2e7d32
style F4 fill:#e8f5e9,stroke:#2e7d32
style F5 fill:#e8f5e9,stroke:#2e7d32
```
**正确使用流式下载避免 OOM:**
```go
// ❌ 错误做法:大文件会把整个内容加载到内存
obj, _ := client.GetObject(ctx, bucketName, "large-backup.sql.gz", minio.GetObjectOptions{})
data, _ := io.ReadAll(obj) // 10GB 文件 → 10GB 内存 = 💥
// ✅ 正确做法:边读边写
obj, _ := client.GetObject(ctx, bucketName, "large-backup.sql.gz", minio.GetObjectOptions{})
defer obj.Close()
dst, _ := os.Create("./backup-latest.sql.gz")
defer dst.Close()
_, err := io.Copy(dst, obj) // 零拷贝,内存占用恒定
```
**STS 临时凭证(RAM Role):**
```go
import "github.com/aliyun/aliyun-sts-go-sdk"
func getTemporaryCredentials() (*sts.Credentials, error) {
client := stssdk.NewClientWithAccessKey("cn-hangzhou", ak, sk)
request := stssdk.CreateAssumeRoleRequest()
request.RoleArn = "acs:ram::<YOUR_ACCOUNT_ID>:role/oss-upload-role"
request.RoleSessionName = "web-session"
request.DurationSeconds = 900 // 15 分钟
response, err := client.AssumeRole(request)
if err != nil {
return nil, err
}
// 前端用这些临时凭据直传 OSS,服务端不暴露 AK/SK
return &response.Credentials, nil
}
```
> **教学提示**:STS 的核心思路是"最小权限原则"——前端只获得一个有时效性的短期令牌,即使被拦截也只能在 15 分钟内使用,且只能用于指定的 Bucket 和路径。
## 附录:快速上手清单
部署新的 OSS 集成时,逐项检查:
- [ ] 创建独立 RAM 子账号(不要直接用主账号 AK)
- [ ] 最小权限授权:只允许目标 Bucket 的操作
- [ ] 服务端和内网都使用 `-internal` Endpoint
- [ ] 文件大小限制前置到中间件层(参考 `MaxMultipartMemory`)
- [ ] 文件名使用 UUID 重命名,防止冲突和路径遍历
- [ ] 生成预签名 URL 时设置合理的过期时间
- [ ] 上传成功后通过 callback 或事件通知更新数据库元信息
- [ ] 配置 Lifecycle 自动清理过期文件
- [ ] 大文件传输加进度条和断点续传支持(客户端侧)
## 关联笔记
- [[GIN/8-file-upload]] — 文件上传后如何存入 OSS,以及 MaxMultipartMemory 的内存优化
- [[GIN/11-static-files]] — 静态文件服务架构中 OSS/S3 + CDN 的集成模式
- [[部署与运维基础]] — 容器环境中磁盘、网络和密钥管理的注意事项