CHAPTER 7 · OBJECT STORAGE
07
假设我们直接把用户照片保存在 Go 服务器的硬盘上,会发生什么?
文件上传不是“把图片放到某个目录”这么简单。照片的生命周期通常比某一台业务服务器更长。
先看最直觉的方案
浏览器上传,Go 后端保存到 /uploads。
硬盘空间不足时怎么办?
服务器损坏后,照片还能找回来吗?
系统扩容成三台服务器后,照片到底保存在哪一台?
用户请求被负载均衡到另一台服务器时,还能访问原来的照片吗?
大量下载图片时,会不会影响 API 接口?
公开图片和私有图片如何控制访问权限?
第一次上传
请求进入服务器 A
照片保存在服务器 A
第二次访问
请求进入服务器 B
服务器 B 找不到照片
本地硬盘不是绝对错误。个人项目、原型系统、内部小工具可以先这样做。但一旦进入多实例、高可用、海量文件和私有访问控制,用户文件就不应该和某一个应用实例绑定。
引出文件存储服务
我们需要把“保存照片”从 Go 服务器里拆出去。
业务服务器
运行 Go 代码
处理登录、权限、接口
读写 MySQL 业务数据
文件存储服务
长期保存用户文件
支持大量文件访问
提供权限和临时访问能力
这种专门用来保存文件的云服务,通常叫对象存储。阿里云的对象存储产品叫 OSS,英文是 Object Storage Service。后面我们说 OSS,本质上就是在说“专门保存和分发用户文件的对象存储服务”。
对象存储的价值
对象存储解决的不只是“文件放在哪里”。
容量扩展
文件增长不再直接压垮业务服务器硬盘,也不需要因为照片变多而重装应用服务。
多实例共享
Go 服务 A、B、C 都通过同一个 Object Key 访问同一张照片。
可靠性
底层冗余由云服务承担,业务系统仍要负责权限、生命周期和必要备份策略。
访问与分发
图片流量可以直接从 OSS 返回,还可以结合 CDN,减少 API 服务器带宽压力。
权限控制
Private Bucket、RAM 权限和短期签名 URL 可以共同保护私有资源。
运维解耦
业务服务负责逻辑,OSS 负责文件存储和传输,两者生命周期分离。
业务后端负责授权和数据关系 + OSS 负责文件存储和传输
核心概念
在 OSS 中,一张照片是怎样保存的?
OSS
└── album-course-prod
├── users/1001/albums/2001/photo-a.jpg
├── users/1001/albums/2001/photo-b.jpg
└── users/1002/albums/3001/photo-c.jpgOSS_REGION=cn-hangzhou
OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
OSS_BUCKET=album-course-dev地域和 Endpoint 必须匹配。Endpoint、Bucket 名称这类配置不应该散落在业务代码中。
数据库与文件关系
数据库保存稳定对象标识,不保存临时访问地址。
CREATE TABLE photos (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
album_id BIGINT UNSIGNED NOT NULL,
bucket_name VARCHAR(100) NOT NULL,
object_key VARCHAR(500) NOT NULL,
original_name VARCHAR(255) NOT NULL,
file_size BIGINT UNSIGNED NOT NULL,
content_type VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_photos_object_key(object_key),
KEY idx_photos_album_created(album_id, created_at)
);object_key
稳定定位 OSS 对象,签名 URL 过期后仍然有效。
bucket_name
固定单 Bucket 可以不存;多 Bucket、多地域时建议保存。
签名后的完整 URL
不要它是临时访问凭证,会过期,也不适合作为业务数据长期保存。
原始文件名用于展示,Object Key 用于定位对象。Object Key 是资源权限边界的一部分,应由后端生成。
Bucket 权限
相册既有公开照片,也有私有照片,Bucket 应该公开吗?
错误方案
Bucket 设置为 public-read,私有相册只是不在页面上展示。资源私有不能依赖“别人猜不到 URL”。Private Bucket 中也不要随意把单个 Object 设置为 public-read,更不能使用 public-read-write。
上传架构
浏览器上传照片时,文件要不要经过 Go 后端?
Bucket 使用私有权限后,浏览器不能随便往 OSS 写文件。下一步要解决的是:既然文件最终要进 OSS,上传链路应该怎么设计,才能既减少 Go 服务压力,又不绕过后端权限控制?
后端中转
浏览器
↓ 上传 20 MB
Go 后端
↓ 再上传 20 MB
OSS
前端直传
浏览器
↓ 申请上传权限
Go 后端
↓ 返回短期授权
浏览器 → OSS
前端直传 OSS ≠ 前端拥有 OSS 的自由操作权限
严重错误
永久 AccessKey 只能留在后端安全环境,不能返回给前端。
func CreateUploadURL(c *gin.Context) {
var req struct {
ObjectKey string `json:"objectKey"`
ContentType string `json:"contentType"`
}
c.ShouldBindJSON(&req)
uploadURL := SignPutObjectURL(
accessKeyID,
accessKeySecret,
objectKey,
req.ContentType,
)
}临时授权
后端怎样临时授权前端上传?
预签名 PUT URL
前端申请上传地址
↓
后端生成指定 Object Key 的 PUT URL
↓
前端使用 PUT 上传文件
↓
URL 过期后失效
适合单文件和普通照片上传。实现直接,后端能精确控制一个 Object Key。
STS 临时凭证
前端申请临时凭证
↓
后端调用 STS
↓
返回临时 AK/SK/Token
↓
前端通过 OSS SDK 上传
适合多文件、分片上传、断点续传等更复杂场景,权限设计也更复杂。
本课程主线使用预签名 PUT URL。它像一个短期 Bearer 凭证,拿到 URL 的人可以在有效期内执行被授权的上传操作,所以有效期要短,也不要写进普通日志。
{
"fileName": "beijing.jpg",
"contentType": "image/jpeg",
"fileSize": 2457600
}{
"code": "OK",
"message": "success",
"data": {
"uploadId": "019acb25-xxxx",
"objectKey": "users/1001/albums/2001/019acb25.jpg",
"uploadUrl": "https://signed-upload-url...",
"method": "PUT",
"contentType": "image/jpeg",
"expiresIn": 300
}
}Object Key
Object Key 必须由后端生成。
错误请求
{ "objectKey": "users/2002/albums/9999/admin.jpg" }前端只提交
原始文件名
文件大小
Content-Type
后端决定
Bucket
Object Key
上传权限
有效时间
objectKey := fmt.Sprintf(
"users/%d/albums/%d/%s%s",
currentUserID,
albumID,
uuid.NewString(),
safeExtension,
)关键后端代码
申请上传 URL 前,先完成身份、归属和文件信息校验。
func CreateUploadURL(db *gorm.DB, ossService *OSSService) gin.HandlerFunc {
return func(c *gin.Context) {
albumID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req CreateUploadURLRequest
c.ShouldBindJSON(&req)
}
}前端直传代码
上传文件时不带 JWT,申请授权和完成确认时才带 JWT。
async function uploadPhoto(file, albumId, token) {
const authResponse = await fetch(
`/api/albums/${albumId}/photos/upload-url`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
fileName: file.name,
contentType: file.type,
fileSize: file.size,
}),
},
);
const uploadInfo = (await authResponse.json()).data;
await fetch(uploadInfo.uploadUrl, {
method: "PUT",
headers: { "Content-Type": uploadInfo.contentType },
body: file,
});
return fetch(`/api/photo-uploads/${uploadInfo.uploadId}/complete`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
}PUT 到 OSS 时使用的是签名 URL 本身的权限。申请签名 URL 和完成确认仍然回到 Go 后端,因为它们涉及用户身份、相册归属和数据库记录。
上传任务
OSS 上传成功,是否代表照片已经进入相册?
孤儿对象
OSS 上传成功
↓
浏览器断网
↓
MySQL 中没有照片记录
无效记录
MySQL 先创建照片记录
↓
OSS 上传失败
↓
数据库出现无效照片
CREATE TABLE photo_uploads (
id CHAR(36) PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
album_id BIGINT UNSIGNED NOT NULL,
object_key VARCHAR(500) NOT NULL,
expected_size BIGINT UNSIGNED NOT NULL,
content_type VARCHAR(100) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
expires_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_photo_upload_object_key(object_key),
KEY idx_photo_upload_status_expires(status, expires_at)
);完整上传流程
一张照片从选择文件到进入相册,需要经过这些步骤。
下面这组步骤把前面讲过的 JWT、相册归属、Object Key、预签名 URL、前端直传和完成确认串起来。点击任一步,可以看它在整个上传链路中的职责。
浏览器是业务流程的参与者,但不能作为最终可信的数据来源。完成接口必须通过 HeadObject 验证 OSS 对象真实存在,并校验大小和 Content-Type。
私有照片查看
Bucket 是私有的,浏览器怎么展示图片?
{
"code": "OK",
"message": "success",
"data": {
"url": "https://signed-view-url...",
"expiresIn": 300
}
}result := db.
Table("photos AS p").
Joins("JOIN albums AS a ON a.id = p.album_id").
Select("p.object_key").
Where(`
p.id = ?
AND (
a.visibility = ?
OR a.user_id = ?
)
`, photoID, VisibilityPublic, currentUserID).
First(&photo)业务系统决定谁可以查看;OSS 签名决定这次访问权限持续多久。签名 URL 不写入数据库,也不应该长时间缓存。
公开照片方案
公开照片也一定要签名吗?
全部使用 Private Bucket + 签名 URL
权限模型统一,公开转私有更容易立即生效,不会意外暴露原始对象。代价是 URL 会过期,前端需要刷新地址,CDN 缓存设计更复杂。
公开照片使用公开访问地址
URL 稳定,更方便缓存和 CDN 分发。代价是公开转私有更复杂,旧 URL 可能继续有效,权限模型更容易分叉。
本课程为了保持权限模型清晰,统一使用 Private Bucket 和短期签名 URL。大规模公开图片业务可以根据流量、CDN 和缓存需求采用不同方案,但必须重新评估权限和失效策略。
最小权限
后端不应该使用阿里云主账号 AccessKey。
oss:PutObject / GetObject / HeadObject / DeleteObject
按需授予业务服务完成上传、查看、验证和删除所需的最小操作集合。
修改 Bucket ACL / 删除 Bucket / 访问所有 Bucket
不要随意授予这类权限一旦泄露,影响范围会远超相册业务。
本章总结
- 用户文件不应该与某一台业务服务器绑定。
- OSS 将文件容量、可靠性、访问和权限能力从业务服务中独立出来。
- OSS 中通过 Bucket 和 Object Key 定位对象。
- 相册系统建议使用 Private Bucket,Object ACL 不要随意公开。
- 前端直传可以减少业务服务器的带宽和连接压力,但仍必须经过后端授权。
- 永久 AccessKey 只能留在后端安全环境,不能返回给浏览器。
- 普通照片上传可以使用短期 PUT 预签名 URL,复杂上传可以进一步使用 STS。
- Object Key 应由后端生成,数据库保存稳定 Object Key,不保存临时签名 URL。
- OSS 上传完成后,后端仍需验证对象并创建业务记录。
- 私有照片应在业务权限校验后生成短期 GET 签名 URL。
- 后端访问 OSS 应使用 RAM 身份和最小权限。
文件存储方案清楚了,前端页面如何真正调通这些接口?
下一章:Postman 能调用成功,为什么前端页面还是失败?