你正在开发一个带文件上传功能的项目。用户可以上传头像、商品图片、PDF、视频或 Excel,开发环境里最直接的做法,是把文件保存到服务器的某个目录:
/uploads/avatar/123.jpg
本地运行时没有问题,一旦项目部署到云服务器、容器或多实例环境,问题很快就会出现。容器重新发布后文件可能消失,两台服务器之间看不到彼此的文件,磁盘容量需要手动扩容,文件下载还会占用应用服务器的网络和连接资源。
这类问题通常不是靠“再写一个文件上传接口”解决,而是需要把文件存储从应用服务器中拆出来,交给专门的对象存储服务。阿里云 OSS,也就是 Object Storage Service,解决的正是这类非结构化数据存储问题。它适合保存图片、音视频、文档、安装包、日志归档、模型文件和备份数据,但不用于替代 MySQL,也不具备传统文件系统完整的目录、文件锁和随机修改语义。
理解 OSS 的关键,是建立一个完整模型:
业务数据库负责管理“这个文件属于谁、能否访问、当前是什么状态”
OSS负责保存“文件本身的二进制数据”
接下来我们会先在控制台走通一次完整流程,再进入项目接入、权限设计、代码结构和生产环境中的工程判断。
一、OSS 解决的不是“上传文件”,而是文件存储基础设施问题
假设用户上传了一张头像。一个完整的系统实际上需要解决四件事:
接收用户文件
→ 判断用户是否有权上传
→ 将文件稳定保存
→ 在后续请求中安全地访问文件
普通的本地磁盘方案把第四步之前的所有责任都放在应用服务器上。应用代码不仅负责业务,还要负责磁盘空间、目录规划、备份、迁移、多实例同步和文件分发。一旦系统开始扩容,这种耦合会迅速变成负担。
接入 OSS 后,系统结构会变成:
客户端
↓
业务服务器:登录验证、文件校验、业务记录
↓
OSS:保存文件内容
↓
数据库:保存文件与用户、订单或文章之间的关系
日常项目中,OSS 常见于以下场景:
用户头像、封面图、聊天图片和附件;
电商商品图、宣传视频和电子合同;
后台导出的 Excel、CSV 或 PDF;
安装包、前端静态资源和公开下载文件;
数据备份、日志归档和历史文件;
AI 项目中的数据集、模型文件和生成结果。
它不适合保存需要频繁按字段查询和修改的数据。例如订单金额、用户昵称、库存数量仍然应该存入数据库。也不适合直接当作共享磁盘使用:OSS 中的对象通常以完整文件为单位上传、读取或覆盖,而不是像操作本地文件一样随时修改其中几个字节。
一个常见的错误设计,是把所有文件都公开放在 OSS 中,然后把完整 URL 存入数据库。这样虽然开发速度快,但业务会逐渐失去对权限、域名、文件状态和存储位置的控制。更合理的设计是保存稳定的 Bucket 和 Object Key,根据访问场景动态生成 URL。
二、先在控制台走通一次最小闭环
在写代码前,建议先用控制台完成一次“创建 Bucket、上传文件、访问文件”的闭环。这样以后遇到问题时,你能够判断错误来自 OSS 配置、权限策略、网络环境还是应用代码,而不是把所有问题都交给 AI 猜测。
登录阿里云控制台后,搜索“对象存储 OSS”,按照页面提示开通服务。进入 OSS 管理控制台后,打开 Bucket 列表并创建一个 Bucket。当前控制台的具体布局可能调整,但核心配置不会脱离 Bucket 名称、地域、存储类型、冗余类型和访问控制。
创建 Bucket 时应该如何选择
Bucket 名称必须在整个 OSS 范围内全局唯一,创建后不能修改,只能使用小写字母、数字和短横线。名称还可能出现在访问域名中,因此不要把账号、手机号或内部项目机密写进名称。
测试项目可以使用类似名称:
pan-demo-assets-2026
不要直接使用:
test
images
my-oss
这些名称大概率已经被其他用户占用。
地域 Region表示数据实际存放的数据中心。地域创建后不能修改。如果后端部署在杭州地域的 ECS、函数计算或容器服务中,通常也应把 Bucket 建在杭州,以便后端通过同地域内网 Endpoint 访问 OSS。
开发阶段不确定部署位置时,也不要随便选择地域。地域会影响网络路径、Endpoint、访问延迟、数据迁移和部分产品联动。后续若要更换地域,通常需要创建新 Bucket 并迁移数据,而不是修改一个配置项。
存储类型建议从标准存储开始。OSS 还提供低频访问、归档、冷归档和深度冷归档,但这些类型针对访问频率较低、保存时间较长的数据,可能存在最低存储时长、最小计费单位、数据取回费用或解冻时间。对于头像、商品图片、用户附件等正常在线业务,不要为了降低表面上的存储单价而过早使用归档类型。
存储冗余类型主要分为本地冗余和同城冗余。同城冗余会把数据放在同一地域的多个可用区中,能降低单个可用区故障带来的影响,但成本通常更高。学习项目可以使用默认配置,关键生产数据则需要根据可用性要求评估。
读写权限建议保持私有。当前 OSS 还提供“阻止公共访问”机制,开启后会忽略已有的公共访问权限,并阻止继续配置公共读或公共读写。自 2025 年 10 月起,OSS 也逐步调整通过 API、SDK 和工具创建 Bucket 时的默认公共访问保护行为,因此你可能会遇到“已经设置公共读但仍然无法匿名访问”的情况。
不要为了省去签名逻辑,就把整个 Bucket 改成公共读写。公共读写意味着匿名用户可能向其中写入文件,几乎不应出现在正常业务系统里。
上传并识别一个 Object
Bucket 创建后,进入:
Bucket 列表
→ 目标 Bucket
→ 文件管理
→ 文件列表
→ 上传文件
上传一个名为 hello.txt 的测试文件。控制台上传本质上也是在调用 OSS 的上传接口,只是控制台替你完成了签名和请求构造。
上传完成后,你需要记录四个信息:
Region: cn-hangzhou
Bucket: pan-demo-assets-2026
Object Key: demo/hello.txt
Endpoint: oss-cn-hangzhou.aliyuncs.com
其中 demo/hello.txt 看起来像“demo 文件夹中的 hello.txt”,但 OSS 内部没有真正的目录树。整个字符串就是 Object Key,斜杠只是名称的一部分,控制台根据斜杠把对象展示成类似文件夹的结构。
现在你已经完成了第一个最小闭环:
创建 Bucket
→ 上传 Object
→ 获得 Object Key
→ 使用控制台查看或下载 Object
下一步才是把这套能力接入项目。
三、理解 OSS 的核心概念:配置从哪里来,权限到底怎样生效
第一次接入 OSS 时,开发者通常会面对一组看起来十分相似的配置:
Bucket
Region
Endpoint
Bucket Domain
AccessKey ID
AccessKey Secret
Role ARN
SecurityToken
Object Key
这些配置不是同一层面的东西。Bucket、Region 和 Object Key 用来确定“数据在哪里”;Endpoint 和 Bucket 域名用来确定“请求发到哪里”;AccessKey、RAM 用户和 RAM 角色用来确定“请求者是谁”;RAM Policy、Bucket Policy 和 ACL 则用来确定“这个请求者允许做什么”。
因此,一次 OSS 请求可以抽象成下面的判断过程:
请求者是谁
→ 使用哪个凭证证明身份
→ 请求发往哪个地域和 Bucket
→ 准备对哪个 Object 执行什么操作
→ 权限策略是否允许
→ OSS 执行操作并返回结果
例如,后端准备上传一张用户头像:
RAM 用户:project-prod-backend
使用凭证:AccessKey 或云上临时凭证
Region:cn-hangzhou
Endpoint:oss-cn-hangzhou.aliyuncs.com
Bucket:project-prod-private
Object Key:users/42/avatar/8d3f.jpg
Action:oss:PutObject
OSS 接收到请求后,会检查签名是否合法、Endpoint 是否属于该 Bucket 所在地域、凭证对应的身份是否存在,以及该身份是否拥有向指定 Object Key 执行 PutObject 的权限。任何一项不匹配,都可能得到 403、签名错误、Endpoint 错误或 AccessKey 无效等结果。
先建立 OSS 的资源层级
OSS 的资源关系可以理解为:
阿里云账号
└── Region
└── Bucket
├── Object A
├── Object B
└── Object C
这里需要特别说明,Region 并不是 Bucket 的上级管理容器,但一个 Bucket 在创建时必须选择一个 Region,并且创建后不能直接修改所在地域。Region 决定数据实际存储的位置,也决定程序应该使用哪个 Endpoint 访问该 Bucket。
一个具体 Object 可以抽象为:
Object {
key: "users/42/avatar/8d3f.jpg",
data: 文件二进制内容,
metadata: {
Content-Type: "image/jpeg",
Content-Length: 183420,
ETag: "...",
Last-Modified: "..."
}
}
Bucket是存放 Object 的逻辑容器。Bucket 名称需要全局唯一,一个账号可以创建多个 Bucket,用于隔离不同环境、业务和权限边界。
例如:
project-dev-private
project-test-private
project-prod-private
project-prod-public
project-prod-backup
是否拆分 Bucket,不应该只根据“图片、视频、文档”这样的文件类型决定。更重要的判断是:
权限是否不同
地域是否不同
生命周期是否不同
加密方式是否不同
是否需要独立计费和监控
是否需要隔离开发与生产环境
例如,生产用户附件与开发测试文件不应该混在同一个 Bucket 中。否则测试代码可能误删生产文件,开发人员也可能因为获得测试环境权限而间接获得生产数据权限。
Object是 OSS 实际保存的数据单元。图片、视频、压缩包和 JSON 文件在 OSS 看来没有本质区别,都是二进制数据加元数据。
Object Key是 Object 在 Bucket 内的唯一名称。例如:
avatar/a.jpg
avatar/b.jpg
document/a.jpg
这三个字符串表示三个不同的 Object。
OSS 控制台会把 / 展示成目录层级,但 OSS 本身并不真正维护传统文件系统中的文件夹。下面这个 Key:
user-content/42/2026/08/04/550e8400-e29b-41d4-a716-446655440000.jpg
本质上是一个完整字符串,不是一个真正经过五层目录保存的本地文件。
Object Key 中的前缀仍然非常重要,因为它可以用于:
按业务分类
限制RAM权限范围
配置生命周期规则
批量列举文件
排查孤儿文件
统计不同业务的数据量
如果再次向同一个 Bucket 上传完全相同的 Object Key,通常会覆盖当前对象;开启版本控制后,则可能保留多个历史版本。因此生产项目不应直接使用用户上传的原始文件名作为唯一 Object Key。
更合理的生成规则是:
业务前缀/租户或用户ID/日期/随机ID.扩展名
例如:
user-content/42/2026/08/04/550e8400-e29b-41d4-a716-446655440000.jpg
数据库可以另外保存原始文件名:
original_name = "暑假旅游照片.jpg"
object_key = "user-content/42/2026/08/04/550e8400-e29b-41d4-a716-446655440000.jpg"
原始文件名负责展示,Object Key 负责在 OSS 中稳定定位资源。
从 OSS 控制台获取项目需要的配置
一个普通后端项目通常需要准备以下 OSS 配置:
| 配置项 | 示例 | 是否敏感 | 用途 |
|---|---|---|---|
| Bucket Name | project-prod-private |
否 | 指定操作哪个 Bucket |
| Region ID | cn-hangzhou |
否 | SDK 签名和地域配置 |
| Endpoint | oss-cn-hangzhou.aliyuncs.com |
否 | SDK 连接 OSS 的服务地址 |
| Internal Endpoint | oss-cn-hangzhou-internal.aliyuncs.com |
否 | 同地域阿里云内网访问 |
| Bucket Domain | project-prod-private.oss-cn-hangzhou.aliyuncs.com |
否 | 访问具体 Bucket 的域名 |
| AccessKey ID | LTAI... |
是 | 标识访问身份 |
| AccessKey Secret | ... |
高度敏感 | 对请求进行签名 |
| Role ARN | acs:ram::...:role/... |
是 | STS 扮演角色时指定目标角色 |
| SecurityToken | 临时字符串 | 高度敏感 | 使用 STS 临时凭证时必须携带 |
| Object Key Prefix | user-content/ |
否 | 限制和组织文件范围 |
要查看已经创建的 Bucket,可以进入:
阿里云控制台
→ 对象存储 OSS
→ Bucket 列表
→ 单击目标 Bucket
→ 概览
→ 访问端口
在“访问端口”区域,可以查看 Bucket 所在地域对应的 Endpoint,以及该 Bucket 的访问域名。控制台展示的菜单名称未来可能略有调整,但核心位置仍然是目标 Bucket 的概览或基础信息页面。
假设控制台显示:
Bucket:project-prod-private
地域:华东1(杭州)
Region ID:cn-hangzhou
外网 Endpoint:oss-cn-hangzhou.aliyuncs.com
内网 Endpoint:oss-cn-hangzhou-internal.aliyuncs.com
Bucket 外网域名:
project-prod-private.oss-cn-hangzhou.aliyuncs.com
这几项之间存在明确的组成关系:
Region ID
cn-hangzhou
专用 Region ID
oss-cn-hangzhou
Endpoint
oss-cn-hangzhou.aliyuncs.com
Bucket 域名
project-prod-private.oss-cn-hangzhou.aliyuncs.com
官方文档将 Region ID、OSS 专用 Region ID、Endpoint 和 Bucket 域名区分为四个概念:Region ID 通常用于 SDK 的地域配置和 V4 签名;Endpoint 用于建立与 OSS 服务的网络连接;Bucket 域名则定位到具体 Bucket,可用于资源访问、签名 URL 和自定义域名解析等场景。
外网 Endpoint 和内网 Endpoint 怎样选择
外网 Endpoint 适合:
本地开发电脑访问 OSS
非阿里云服务器访问 OSS
跨地域服务器访问 OSS
浏览器或移动客户端访问 OSS
杭州地域的外网 Endpoint 形态为:
oss-cn-hangzhou.aliyuncs.com
内网 Endpoint 适合:
杭州地域 ECS
杭州地域容器服务
杭州地域函数计算
其他能够访问杭州阿里云内网的计算资源
对应形态为:
oss-cn-hangzhou-internal.aliyuncs.com
同地域阿里云计算资源使用内网 Endpoint,通常可以减少公网绕行,并避免相关外网流量费用;如果应用不在对应的阿里云内网中,则不能因为“内网更便宜”就强行使用内网 Endpoint。
一个常见配置方式是:
app:
oss:
bucket: project-prod-private
region: cn-hangzhou
endpoint: https://oss-cn-hangzhou.aliyuncs.com
如果生产服务器与 Bucket 位于同一个阿里云地域,可以通过生产环境变量覆盖:
OSS_ENDPOINT=https://oss-cn-hangzhou-internal.aliyuncs.com
需要注意,SDK 版本之间的初始化方式可能不同。部分新版 SDK 只要求填写 Region,然后自动选择默认外网 Endpoint;部分旧版 SDK 或特定语言 SDK 仍会显式要求传入 Endpoint。不能把某一种语言、某一个 SDK 版本的参数格式直接复制到另一套 SDK 中。
Endpoint 和 Bucket 域名为什么不能混用
SDK 初始化通常使用地域级 Endpoint:
https://oss-cn-hangzhou.aliyuncs.com
而具体 Bucket 的资源域名通常是:
https://project-prod-private.oss-cn-hangzhou.aliyuncs.com
它们的区别可以理解为:
Endpoint:
我要连接杭州地域的 OSS 服务
Bucket 域名:
我要访问杭州地域中名为 project-prod-private 的 Bucket
请求最终仍然需要包含 Bucket 信息。SDK 会根据 Bucket 名称、Endpoint 和寻址模式组成真正的请求地址,并计算签名。
如果程序使用了错误地域的 Endpoint,例如 Bucket 实际位于杭州,却连接北京 Endpoint,就可能出现:
The bucket you are attempting to access
must be addressed using the specified endpoint
此时不应优先怀疑 AccessKey,而应该同时检查:
Bucket 所在地域
Region ID
Endpoint
Bucket 名称
SDK 使用的签名版本
请求最终 Host
官方的 403 排查文档也将错误 Endpoint 列为常见原因之一。
RAM 用户、RAM 角色和 AccessKey 分别是什么
理解 OSS 权限时,需要先区分三个身份概念。
阿里云主账号
RAM 用户
RAM 角色
阿里云主账号是整个阿里云账号的根身份,默认拥有账号下资源的完整控制能力。主账号适合完成账号级管理,不适合作为业务应用长期运行时使用的身份。
RAM 用户是一种长期身份,可以代表一个员工、自动化脚本或业务应用。例如:
developer-zhang
project-test-backend
project-prod-backend
oss-backup-job
新创建的 RAM 用户默认没有任何权限,必须为其附加系统策略或自定义策略,才能访问 OSS 等云资源。RAM Policy 可以附加在 RAM 用户、用户组或 RAM 角色上。
RAM 角色本身没有长期登录密码或永久 AccessKey。一个受信任的 RAM 用户、云服务或其他身份可以“扮演”该角色,从而获得一组短期有效的临时凭证。
它们之间可以这样理解:
RAM 用户
像一个长期存在的员工账号
RAM 角色
像一个可以临时领取的岗位权限
AccessKey
像程序身份使用的长期门禁卡
STS 临时凭证
像有过期时间、限制区域和权限的临时通行证
AccessKey ID 和 AccessKey Secret 的关系
一个永久 AccessKey 包含:
AccessKey ID
AccessKey Secret
AccessKey ID 用于标识访问身份,AccessKey Secret 用于对请求进行加密签名。两者共同使用,OSS 才能验证请求是否由对应身份发出。
不要把它误解为普通账号密码。程序不会在每次请求中直接发送 AccessKey Secret,而是使用 Secret 对请求内容进行签名,再把签名结果发送给 OSS。
AccessKey Secret 只会在创建时展示,关闭页面后通常不能再次查看。如果丢失,只能创建新 AccessKey,并逐步替换旧凭证。
阿里云将 AccessKey 分为主账号 AccessKey 和 RAM 用户 AccessKey。官方明确不推荐使用主账号 AccessKey,因为主账号 AccessKey 默认拥有账号下极高权限,一旦泄露,影响范围可能覆盖整个阿里云账号。更合理的做法是为不同业务应用创建独立 RAM 用户,并分别授予最小权限。
例如:
project-dev-backend
只允许操作 project-dev-private
project-prod-backend
只允许操作 project-prod-private/user-content/*
oss-backup-job
只允许读取生产数据并写入备份Bucket
不要让多个应用共用同一组 AccessKey。否则某个应用泄露凭证后,很难快速判断影响范围,也难以单独禁用和轮换。
怎样创建后端应用使用的 RAM 用户和 AccessKey
进入:
阿里云控制台
→ 访问控制 RAM
→ 身份管理
→ 用户
→ 创建用户
为后端应用创建一个专用用户,例如:
登录名称:project-prod-backend
显示名称:生产环境后端OSS身份
访问方式:使用永久 AccessKey 访问
创建完成后,保存:
AccessKey ID
AccessKey Secret
不要给这个 RAM 用户开启控制台登录,除非它还需要由真人登录管理。一个纯后端应用通常只需要程序访问。
创建后的 AccessKey 不应直接写进:
application.yml
application.properties
Dockerfile
docker-compose.yml
GitHub仓库
前端代码
移动端安装包
本地开发可以使用环境变量:
export OSS_ACCESS_KEY_ID="..."
export OSS_ACCESS_KEY_SECRET="..."
生产服务器可以使用:
systemd EnvironmentFile
Docker Compose env_file
CI/CD Secret
云平台密钥管理服务
ECS实例RAM角色
容器工作负载身份
代码只负责从凭证提供器或环境变量读取,不负责保存真实密钥。OSS 官方 SDK 快速入门同样建议从环境变量读取凭证,避免把 AccessKey 明文写入代码。
ACL、RAM Policy、Bucket Policy 和阻止公共访问怎样共同工作
OSS 权限最容易混淆,是因为它同时存在“身份侧权限”和“资源侧权限”。
可以先把四种机制放到一张图中:
RAM Policy
绑定在用户、用户组或角色上
回答:这个身份能做什么
Bucket Policy
绑定在Bucket上
回答:哪些身份可以访问这个Bucket
Bucket/Object ACL
绑定在Bucket或Object上
回答:资源默认是私有、公共读还是公共读写
阻止公共访问
账号或Bucket层面的安全开关
回答:是否一律阻止匿名公共访问
ACL:定义资源最基本的公开程度
Bucket ACL 常见值包括:
private
public-read
public-read-write
Object ACL 常见值包括:
default
private
public-read
public-read-write
default 表示 Object 继承 Bucket ACL。
如果 Object 显式设置了非 default ACL,Object ACL 的判断优先于 Bucket ACL。例如 Bucket 是私有的,但某个 Object 被设置为 public-read,该 Object 原本可以被匿名读取。
当前 OSS 创建 Bucket 时通常默认开启“阻止公共访问”。开启后,Bucket ACL 只能设置为私有,Object ACL 也只能设置为 private 或 default;已有的公共访问权限会被忽略,同时不能继续创建新的公共访问配置。
查看或修改 Bucket ACL 的控制台路径为:
OSS控制台
→ Bucket列表
→ 目标Bucket
→ 权限控制
→ 读写权限
官方文档将 Bucket ACL 定义为 Bucket 级别的公开或私有访问控制,并说明 Object 未单独指定 ACL 时会继承 Bucket ACL。
对于普通业务系统,推荐配置是:
阻止公共访问:开启
Bucket ACL:private
Object ACL:default或private
不要把 public-read-write 当成“上传和下载都方便”。它表示匿名互联网用户可能拥有读写权限,可能导致恶意上传、文件篡改、数据泄漏和费用异常。
RAM Policy:给应用身份授予权限
RAM Policy 绑定在 RAM 用户、用户组或角色上。
例如:
project-prod-backend
→ 允许上传、读取和删除
→ project-prod-private/user-content/*
oss-report-reader
→ 只允许读取
→ project-prod-private/reports/*
RAM 支持系统策略和自定义策略。系统策略由阿里云预先定义,例如 AliyunOSSFullAccess;自定义策略则可以限制到某一个 Bucket、某一个前缀和某几个操作。官方建议遵循最小权限原则。
AliyunOSSFullAccess 可以用于临时排查“是否因为权限导致请求失败”,但不适合直接作为长期生产权限,因为它通常允许身份管理账号下大量 OSS 资源。
创建自定义策略可以进入:
RAM控制台
→ 权限管理
→ 权限策略
→ 创建权限策略
→ 脚本编辑
创建完成后,再进入:
身份管理
→ 用户
→ 找到目标RAM用户
→ 新增授权或添加权限
→ 选择自定义策略
新创建的 RAM 用户没有权限,需要完成授权后才能正常访问 OSS。
Bucket Policy:从 Bucket 一侧授权
Bucket Policy 绑定在具体 Bucket 上,适合处理:
允许另一个阿里云账号访问
允许某个RAM用户访问
按IP限制访问
按VPC限制访问
跨部门或跨账号共享
匿名公共访问
Bucket Policy 可以理解为:
站在这个 Bucket 的角度,哪些身份能够对哪些资源执行哪些操作。
控制台路径通常是:
OSS控制台
→ Bucket列表
→ 目标Bucket
→ 权限控制
→ Bucket授权策略
可以使用图形化方式添加,也可以直接编辑策略脚本。官方文档将 Bucket Policy 定义为附加在 Bucket 上的授权策略,可用于其他账号、RAM 用户、匿名用户以及基于 IP、VPC 的限制。
一个项目只有本账号后端访问 OSS 时,通常使用 RAM Policy 已经足够。Bucket Policy 更常用于跨账号、跨部门、资源侧显式限制等场景。
阻止公共访问:防止错误配置造成匿名暴露
“阻止公共访问”不是另一种普通 ACL,而是更高层的安全保护机制。
它可以配置在:
账号级
Bucket级
接入点级
Object FC接入点级
高层设置会覆盖低层设置。账号级开启后,即使某个 Bucket 试图配置公共 ACL 或允许匿名访问的 Bucket Policy,公共访问仍然会被阻止。
Bucket 级控制台路径为:
OSS控制台
→ Bucket列表
→ 目标Bucket
→ 权限控制
→ 阻止公共访问
普通用户上传文件、合同、头像原图、内部报表等业务,建议保持开启。只有确定需要匿名公开访问,并且已经完成文件隔离、防盗链、费用控制和内容安全评估时,才考虑关闭。
怎样为后端配置最小 OSS 权限
一个后端应用不应该默认拥有整个 OSS 的完整管理权限。应该先列出它实际需要调用的接口。
例如,一个普通文件服务可能只需要:
上传:oss:PutObject
下载:oss:GetObject
删除:oss:DeleteObject
列举:oss:ListObjects
OSS 官方 SDK 快速入门指出,上传、下载和删除分别需要 oss:PutObject、oss:GetObject 和 oss:DeleteObject 权限;列举 Bucket 中的 Object 则需要 oss:ListObjects 权限。
假设后端只允许操作:
Bucket:
project-prod-private
前缀:
user-content/*
可以创建类似下面的 RAM 自定义策略:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"oss:ListObjects"
],
"Resource": [
"acs:oss:*:*:project-prod-private"
],
"Condition": {
"StringLike": {
"oss:Prefix": [
"user-content/*"
]
}
}
},
{
"Effect": "Allow",
"Action": [
"oss:PutObject",
"oss:GetObject",
"oss:DeleteObject"
],
"Resource": [
"acs:oss:*:*:project-prod-private/user-content/*"
]
}
]
}
这段策略需要从四个字段理解。
Effect 表示允许还是拒绝:
Allow
Deny
Action 表示允许调用哪些 OSS 操作:
oss:PutObject
oss:GetObject
oss:DeleteObject
oss:ListObjects
Resource 表示权限作用于哪些资源:
Bucket本身:
acs:oss:*:*:project-prod-private
Bucket中的Object:
acs:oss:*:*:project-prod-private/*
限制到前缀后:
acs:oss:*:*:project-prod-private/user-content/*
Condition 表示只有满足附加条件时权限才生效,例如限制列举的 Object 前缀、来源 IP、VPC 或访问协议。
需要特别注意,ListObjects 是 Bucket 级操作,因此 Resource 通常指向 Bucket 本身;GetObject、PutObject 和 DeleteObject 是 Object 级操作,因此 Resource 指向 Bucket 中的 Object 路径。OSS 的授权语法会根据具体 API 映射到相应 Action。
如果后端不需要列举文件,而是始终从数据库中读取确定的 Object Key,那么可以不授予 oss:ListObjects。这能减少应用扫描整个前缀的能力。
如果业务不允许删除文件,也不应授予:
oss:DeleteObject
如果只是下载服务,则可以只保留:
oss:GetObject
这就是最小权限的真正含义:不是给一个“看起来够小”的系统策略,而是根据应用实际调用的 API,一项项确定 Action 和 Resource。
配置完成后,可以使用临时测试验证:
允许的Object Key上传成功
其他前缀上传失败
允许的Object可以读取
没有DeleteObject权限时删除失败
没有ListObjects权限时列举失败
不能只验证一次正常上传。权限测试应该同时验证“该允许的成功”和“该拒绝的确实失败”。
预签名 URL 和 STS 临时凭证解决的是两个不同问题
私有 Bucket 中的文件不能直接匿名访问,但有些业务又需要让浏览器或移动端在短时间内上传、下载文件。
常见方案有两种:
预签名 URL
STS 临时访问凭证
预签名 URL:临时授权一次具体请求
预签名 URL 是后端使用自己的 OSS 身份,为某个具体请求提前计算签名。
例如下载:
请求方法:GET
Bucket:project-prod-private
Object Key:contracts/42/contract.pdf
有效期:5分钟
后端生成一个带签名参数的 URL:
https://project-prod-private...
?Expires=...
&OSSAccessKeyId=...
&Signature=...
客户端不需要知道 AccessKey Secret,只需要访问这条 URL。
它适合:
下载一份私有文件
上传到一个确定的Object Key
限制请求方法
设置较短有效期
不希望客户端获得通用OSS权限
私有 Object 的预签名 URL 在过期时间内可以被持有者使用,过期后失效;在有效期内通常可以被重复使用。因此,生成 URL 前必须先完成业务权限校验,URL 有效期也不应过长。
正确下载流程应该是:
客户端提交业务 fileId
→ 后端查询文件记录
→ 验证当前用户能否访问
→ 获取Bucket和Object Key
→ 生成短期GET预签名URL
→ 返回客户端
不应设计成:
客户端自由提交Object Key
→ 后端直接生成下载URL
否则用户可能通过猜测 Object Key 获取不属于自己的文件。
预签名 URL 自身包含访问能力,日志、聊天记录、浏览器插件和第三方统计脚本都可能泄露它。因此不要:
把签名URL永久保存到数据库
把完整签名URL写进普通业务日志
把签名有效期设置成几年
把用户无权访问的Object签名后返回
STS:临时获得一组受限制的 OSS 身份
STS 临时凭证更像一套短期 AccessKey,通常包含:
AccessKeyId
AccessKeySecret
SecurityToken
Expiration
客户端必须同时使用这三项凭证信息:
AccessKeyId
AccessKeySecret
SecurityToken
其中 SecurityToken 不能省略。凭证过期后会自动失效,需要重新向业务后端申请。
STS 适合:
客户端需要上传多个文件
需要分片上传
需要断点续传
需要在短时间内执行多次OSS操作
不同用户需要不同前缀权限
典型流程是:
1. 创建用于调用STS的RAM用户
2. 授予该RAM用户AssumeRole权限
3. 创建RAM角色
4. 为RAM角色配置OSS最小权限
5. 后端调用AssumeRole
6. 获取STS临时凭证
7. 返回给通过业务鉴权的客户端
8. 客户端使用临时凭证直接访问OSS
官方 OSS 临时凭证流程要求先创建 RAM 用户,为其授予 AliyunSTSAssumeRoleAccess,再创建 RAM 角色并为角色附加 OSS 权限策略。后端调用 AssumeRole 后,可获得临时 AccessKey、安全令牌和过期时间。
例如,可以让临时角色只能上传到:
project-prod-private/user-content/42/*
而不能:
读取其他用户文件
列举整个Bucket
删除已有文件
上传到system/目录
修改Bucket配置
这里需要同时解决两个层面的授权:
业务系统授权:
当前登录用户是否有资格申请上传权限
OSS授权:
这组STS凭证可以操作哪些Bucket、前缀和Action
不能因为 STS 权限已经限制到某个前缀,就省略业务登录和权限校验。后端仍然需要验证当前用户身份,并由后端生成受控的 Object Key。
两种方案怎样选择
| 场景 | 推荐方案 |
|---|---|
| 下载一个私有文件 | GET 预签名 URL |
| 上传一个小文件到确定 Key | PUT 预签名 URL |
| 一次上传多个文件 | STS |
| 分片上传、断点续传 | STS |
| 浏览器只执行一次确定操作 | 预签名 URL |
| 客户端短期执行一组受限操作 | STS |
| 后端中转上传 | 后端 RAM 身份,不需要向客户端发凭证 |
无论使用预签名 URL 还是 STS,都不能把永久 AccessKey 放到前端代码中。
浏览器直接访问 OSS 时还需要配置 CORS,但 CORS 只决定浏览器是否允许跨域请求,不决定请求者是否拥有 OSS 权限。最终访问仍然需要通过签名 URL、STS、RAM Policy、Bucket Policy 或 ACL 的权限判断。
最终应该整理出怎样的 OSS 配置
完成控制台和权限配置后,可以整理一份项目配置清单:
OSS_BUCKET_NAME=project-prod-private
OSS_REGION=cn-hangzhou
OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
OSS_ACCESS_KEY_ID=...
OSS_ACCESS_KEY_SECRET=...
OSS_UPLOAD_PREFIX=user-content/
OSS_DOWNLOAD_URL_TTL_SECONDS=300
如果使用 STS,再增加:
OSS_STS_ENDPOINT=sts.cn-hangzhou.aliyuncs.com
OSS_ROLE_ARN=acs:ram::<account-id>:role/project-upload-role
OSS_STS_SESSION_SECONDS=900
如果生产服务器与 OSS 位于同一阿里云地域,可以根据 SDK 能力使用内网 Endpoint:
OSS_ENDPOINT=https://oss-cn-hangzhou-internal.aliyuncs.com
其中可以进入代码仓库的通常是:
Bucket名称
Region
Endpoint
默认前缀
签名有效期
不能进入代码仓库的是:
AccessKey Secret
STS SecurityToken
真实生产环境密码
包含有效签名的URL
正式接入代码前,应该完成一次配置核对:
Bucket 名称是否正确
Bucket 是否属于预期环境
Region 是否与 Bucket 一致
Endpoint 是否与网络环境一致
RAM 用户是否为应用专用身份
是否误用了主账号 AccessKey
RAM Policy 是否限制到指定 Bucket
是否可以进一步限制到 Object 前缀
是否授予了不必要的删除或管理权限
阻止公共访问是否保持开启
Bucket ACL 是否为 private
AccessKey 是否通过环境变量或凭证提供器读取
浏览器直传是否使用预签名 URL 或 STS
STS 是否包含 SecurityToken 和 Expiration
签名 URL 是否在生成前完成业务鉴权
当这份清单全部明确后,OSS 才不再是一组从控制台复制出来的字符串,而是一套可以解释、验证和维护的存储权限系统。
四、OSS 在项目中的正确位置:输入、状态和数据流
接入 OSS 时,最重要的工程判断不是选择哪个 SDK 方法,而是决定文件经过谁上传,以及业务状态如何与 OSS 保持一致。
方案一:文件经过业务服务器上传
数据流如下:
用户选择文件
→ 前端提交 multipart/form-data
→ 后端验证登录、大小和类型
→ 后端生成 Object Key
→ 后端把文件流上传到 OSS
→ OSS 返回成功
→ 后端写入文件业务记录
→ 前端获得文件 ID
这个方案最大的优点是逻辑清楚。所有上传都经过后端,权限、文件大小、业务状态和异常处理集中在一处,比较适合文件不大、访问量有限、团队刚开始接触 OSS 的项目。
它的问题也很明确:文件流量会经过业务服务器。大文件上传时,应用服务器需要维持更长的连接,并承担数据转发压力。如果代码先把整个文件读入字节数组,还可能造成较高的内存占用。
学习阶段和普通后台系统可以从该方案开始,但上传代码应直接传递 InputStream,不要无必要地调用:
file.getBytes()
再把完整字节数组交给 OSS。
方案二:客户端直接上传 OSS
数据流如下:
用户选择文件
→ 前端向后端申请上传授权
→ 后端验证用户和业务条件
→ 后端生成 Object Key
→ 后端返回 STS 临时凭证或预签名上传 URL
→ 前端直接向 OSS 上传
→ 前端通知后端上传完成
→ 后端确认对象存在并更新业务状态
客户端直传绕开了业务服务器的数据中转,适合图片较多、大文件上传、移动端上传或高并发场景。官方推荐的 STS 流程中,永久 AccessKey 只保存在可信服务端,客户端获得的是有过期时间和权限边界的临时凭证。
但它不是简单地把上传代码从后端复制到前端。你还需要处理:
上传授权是否被其他用户滥用;
Object Key 是否只能写入当前用户的前缀;
凭证和 URL 的过期时间;
前端显示成功但后端没有业务记录;
前端中断后留下未完成记录;
用户重复提交或覆盖同一 Object;
浏览器 CORS 预检请求;
文件大小、类型和数量约束。
CORS 只解决浏览器是否允许跨域发送请求,不等于用户已经拥有 OSS 权限。真正的访问权限仍然由签名、RAM Policy、Bucket Policy 和 ACL 决定。
不要把上传过程简化成一个布尔值
真实业务中,文件状态建议至少包含:
PENDING
→ UPLOADING
→ READY
失败时:
PENDING / UPLOADING
→ FAILED
删除时:
READY
→ DELETING
→ DELETED
以客户端直传为例,后端先创建一条 PENDING 记录,再向客户端颁发上传权限。客户端上传完成后调用确认接口,后端检查对象信息并将记录改成 READY。
数据库可以保存:
file_asset
- id
- owner_id
- bucket
- object_key
- original_name
- content_type
- size
- checksum
- status
- created_at
- deleted_at
业务表只引用 file_asset.id:
user.avatar_file_id
product.cover_file_id
article.attachment_file_id
这样以后即使更换 Bucket、增加自定义域名或调整 CDN,业务表也不需要保存和批量修改大量完整 URL。
上传回调也可以用于通知业务服务器,但需要理解它的边界:回调成功或失败不影响文件已经保存到 OSS。也就是说,不能把“业务回调失败”等同于“文件没有上传成功”。
因此,OSS 与数据库之间不存在天然的分布式事务。你需要主动设计补偿:
OSS 上传成功,数据库写入失败
→ 产生孤儿 Object
→ 定时任务清理超过一定时间且无业务记录的对象
数据库记录成功,OSS 上传失败
→ 文件状态保持 FAILED
→ 允许重试或重新申请上传授权
这类状态一致性往往比 SDK 调用本身更能区分“演示代码”和真正可进入项目的实现。
五、以 Java 后端为例理解代码结构
下面使用 Spring Boot 和阿里云 OSS Java SDK V1 展示核心结构。代码不是完整项目模板,重点是说明客户端初始化、上传和下载授权中的数据流。官方 Java SDK 支持从环境变量读取凭证,并推荐使用 RAM 用户或角色而不是阿里云主账号的 AccessKey;当前官方示例还会显式配置 V4 签名。
统一初始化 OSS 客户端
这段代码解决的是三个问题:
从哪里获得凭证
→ 请求发往哪个 Endpoint
→ 应用如何管理 OSS 客户端生命周期
配置文件只保存非敏感配置:
app:
oss:
endpoint: https://oss-cn-hangzhou.aliyuncs.com
region: cn-hangzhou
bucket: pan-demo-assets-2026
AccessKey 不写入 application.yml,而是通过运行环境提供:
OSS_ACCESS_KEY_ID
OSS_ACCESS_KEY_SECRET
Spring 配置示例:
import com.aliyun.oss.ClientBuilderConfiguration;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.common.comm.SignVersion;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OssClientConfig {
@Bean(destroyMethod = "shutdown")
public OSS ossClient(
@Value("${app.oss.endpoint}") String endpoint,
@Value("${app.oss.region}") String region
) throws Exception {
EnvironmentVariableCredentialsProvider credentialsProvider =
CredentialsProviderFactory
.newEnvironmentVariableCredentialsProvider();
ClientBuilderConfiguration configuration =
new ClientBuilderConfiguration();
configuration.setSignatureVersion(SignVersion.V4);
return OSSClientBuilder.create()
.endpoint(endpoint)
.region(region)
.credentialsProvider(credentialsProvider)
.clientConfiguration(configuration)
.build();
}
}
这个客户端应该由应用容器统一管理,而不是在每个 Controller 请求中重复创建。初始化过程维护了底层通信配置,应用关闭时再调用 shutdown 释放资源。
真正影响请求是否成功的是四个变量:
credentialsProvider
endpoint
region
bucket
其中 Bucket 不一定要写进客户端,因为同一个客户端可以操作有权限访问的多个 Bucket。对于单 Bucket 项目,把 Bucket 放在业务配置中更容易管理;对于多租户或多业务系统,则应显式传入或通过配置映射选择。
AI 最容易在这里产生的错误包括:
把 AccessKey 直接写死在 Java 文件;
使用阿里云主账号 AccessKey;
Endpoint 与 Bucket 地域不一致;
把 Bucket 域名当成 SDK Endpoint;
每次上传都创建并销毁一个客户端;
在代码仓库中提交生产凭证;
为了让代码运行,直接授予
AliyunOSSFullAccess。
开发测试时可以暂时授予较宽权限验证链路,生产环境则应创建自定义 RAM Policy,只允许应用操作指定 Bucket 和前缀。
上传文件并生成可控的 Object Key
这段代码解决的是:
用户输入文件
→ 后端校验
→ 生成不会冲突的 Key
→ 设置对象元数据
→ 以流的方式上传
→ 返回稳定的 Object Key
import com.aliyun.oss.OSS;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.util.Locale;
import java.util.UUID;
@Service
public class OssStorageService {
private static final long MAX_FILE_SIZE = 20L * 1024 * 1024;
private final OSS ossClient;
private final String bucketName;
public OssStorageService(
OSS ossClient,
@Value("${app.oss.bucket}") String bucketName
) {
this.ossClient = ossClient;
this.bucketName = bucketName;
}
public String upload(long ownerId, MultipartFile file) throws IOException {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("文件不能为空");
}
if (file.getSize() > MAX_FILE_SIZE) {
throw new IllegalArgumentException("文件不能超过 20 MB");
}
String objectKey = buildObjectKey(
ownerId,
file.getOriginalFilename()
);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(normalizeContentType(file.getContentType()));
try (InputStream inputStream = file.getInputStream()) {
PutObjectRequest request =
new PutObjectRequest(bucketName, objectKey, inputStream);
request.setMetadata(metadata);
ossClient.putObject(request);
}
return objectKey;
}
private String buildObjectKey(long ownerId, String originalName) {
String extension = extractExtension(originalName);
return String.format(
"user-content/%d/%s/%s%s",
ownerId,
LocalDate.now(),
UUID.randomUUID(),
extension
);
}
private String extractExtension(String originalName) {
if (originalName == null) {
return "";
}
int dotIndex = originalName.lastIndexOf('.');
if (dotIndex < 0 || dotIndex == originalName.length() - 1) {
return "";
}
String extension = originalName
.substring(dotIndex)
.toLowerCase(Locale.ROOT);
return extension.length() <= 10 ? extension : "";
}
private String normalizeContentType(String contentType) {
if (contentType == null || contentType.isBlank()) {
return "application/octet-stream";
}
return contentType;
}
}
这里真正决定文件行为的不是 putObject 这一行,而是上传前后的约束。
ownerId 被放入 Object Key,可以帮助后续按用户前缀管理和排查,但不能把它当作安全验证。知道某个 Key 不代表用户有权访问该文件,下载前仍然需要查询数据库并验证归属关系。
UUID 用于避免同名覆盖。原始文件名应作为业务元数据保存到数据库,而不是直接作为唯一 Key。
Content-Type 会影响浏览器如何处理文件。例如图片应返回相应的图片 MIME 类型,PDF 应使用 application/pdf。但客户端提交的 Content-Type 不能完全可信,安全要求较高时应根据文件内容、文件头或专门的检测服务再次判断。
MAX_FILE_SIZE 是业务限制,不是 OSS 的平台极限。限制必须在入口层尽早检查,否则用户上传一个超大文件后,系统才在后续阶段拒绝,会浪费连接和流量。
还要注意,扩展名检查不等于文件安全检查。攻击者可以把可执行文件改名为 .jpg。是否需要病毒扫描、内容审核、图片解码验证或文档沙箱,应根据业务风险决定。
生产代码还需要捕获并区分两类异常:
OSSException
→ 请求已经到达 OSS,但因为权限、参数、签名或资源状态被拒绝
ClientException
→ 客户端连接、DNS、超时或本地通信过程出现问题
不要把两类异常都转成“上传失败”。日志中至少应记录错误码、Request ID、Bucket、Object Key 和业务文件 ID,但不要输出 AccessKey 或完整临时凭证。
为私有文件生成下载地址
这段代码解决的是:
用户请求下载
→ 后端检查用户权限
→ 为指定 Object 生成短期 URL
→ 客户端直接从 OSS 下载
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
public URL createDownloadUrl(String objectKey, Duration ttl) {
if (ttl == null || ttl.isNegative() || ttl.isZero()) {
throw new IllegalArgumentException("有效期必须大于 0");
}
Duration safeTtl = ttl.compareTo(Duration.ofMinutes(30)) > 0
? Duration.ofMinutes(30)
: ttl;
Date expiration = Date.from(
Instant.now().plus(safeTtl)
);
return ossClient.generatePresignedUrl(
bucketName,
objectKey,
expiration
);
}
这个方法不能直接暴露成:
GET /files/url?objectKey=任意值
正确流程应该是:
用户提交 fileId
→ 后端查询 file_asset
→ 验证 owner、角色或业务访问权限
→ 从数据库取得 objectKey
→ 生成预签名 URL
也就是说,客户端应传递业务文件 ID,而不是让用户自由指定 Bucket 和 Object Key。
预签名 URL 在过期前可以被持有者访问,因此有效期应根据场景设置。头像展示可能使用公开资源或 CDN;合同、发票和用户隐私文件则应使用较短有效期。不要把带签名的完整 URL 永久保存在数据库,因为其中包含过期时间和签名参数。
如果需要控制文件是浏览器预览还是强制下载,还需要设置正确的 Content-Type 和 Content-Disposition。通过自定义域名访问时,可以更灵活地控制预览、HTTPS 和 CDN;使用 OSS 默认域名时,部分内容可能会被浏览器按附件下载。
六、进入生产环境后需要补上的工程能力
把文件上传到 OSS 只能说明链路打通,不能说明功能已经完成。生产环境至少需要从安全、成本、性能、一致性和可观测性五个方向检查。
安全不是把 Bucket 改成私有就结束了
永久 AccessKey 不应出现在浏览器、移动端安装包或公开代码仓库中。后端运行在 ECS、容器服务等阿里云环境时,可以进一步考虑实例 RAM 角色,由运行环境获取并刷新临时凭证,减少人工维护永久密钥。
浏览器直传应使用 STS 临时凭证或预签名 URL,同时限制:
允许的 Bucket
允许的 Object Key 前缀
允许的操作类型
凭证有效期
文件大小
文件类型
单次或单用户上传数量
不要给客户端返回能够列举整个 Bucket、删除任意文件或覆盖其他用户前缀的凭证。
对于公开图片资源,可以考虑单独的公开资源 Bucket、自定义域名、防盗链和 CDN。对于合同、证件、聊天附件等私密文件,应保持私有,通过后端鉴权和短期签名访问。防盗链依赖 Referer,Referer 可能被伪造,因此不能替代签名鉴权。
成本不只有“存了多少 GB”
OSS 费用通常包含存储、请求、流量和数据处理等部分。文件只要存在于 Bucket 中就会产生存储费用;从 OSS 向公网客户端下载文件可能产生外网流出流量费用;低频和归档类数据还可能产生取回、解冻或不足最低存储时长的费用。
项目初期最应该关注的不是购买资源包,而是先看清数据流:
谁在上传
谁在下载
每天下载多少次
文件平均多大
旧文件多久不再访问
是否存在重复和孤儿文件
生命周期规则可以根据 Object 前缀、标签、最后修改时间或部分访问条件,自动转换存储类型或删除过期数据。例如:
temp/ 下的临时导出文件 7 天后删除
logs/ 下的日志 30 天后转低频
backup/ 下的备份 90 天后转归档
配置冷存储前要理解最低存储时长和取回成本。生命周期的转换顺序也必须满足从较热存储逐步转向更冷存储的规则。
大文件不能继续使用普通小文件思路
小文件可以使用简单上传。文件较大或网络不稳定时,应使用分片上传和断点续传,把一个文件拆成多个 Part,失败时只重新上传失败部分。官方文档通常建议在文件超过约 100 MB 时考虑分片上传,但实际阈值还应结合网络质量、客户端类型和业务容忍度。
分片上传需要额外管理:
uploadId
每个 Part 的编号
每个 Part 的 ETag
上传进度
失败重试
完成合并
未完成分片清理
AI 有时只会并发调用多个上传请求,却没有在最后执行 CompleteMultipartUpload。这样页面可能显示所有分片已经到达,OSS 中却仍没有完整 Object。
删除操作也要设计状态和重试
删除数据库记录和删除 OSS Object 无法放进同一个数据库事务。一个更稳妥的流程是:
READY
→ 标记为 DELETING
→ 调用 OSS 删除
→ 成功后标记 DELETED
如果 OSS 删除失败,可以通过任务队列或定时任务重试。不要先永久删除数据库记录,再发现已经不知道应该删除哪个 Bucket 和 Object Key。
对于重要文件,还可以评估版本控制。启用版本控制后,不指定版本 ID 的删除操作可能只创建删除标记,历史版本仍然存在,因此恢复能力增强,但存储费用和生命周期管理也会更复杂。
日志和监控应能够回答“哪一次请求失败了”
线上排查 OSS 问题时,日志至少应包含:
业务 requestId
用户或租户 ID
文件业务 ID
Bucket
Object Key
操作类型
文件大小
耗时
OSS Request ID
错误码
重试次数
不要在日志中记录 AccessKey Secret、STS Security Token 或完整签名参数。
OSS 可以结合云监控、日志转存、实时日志查询、操作审计和配置审计观察请求、资源和配置变化。对于生产 Bucket,至少需要建立容量、请求错误率、流量和费用异常的观察能力。
最后按真实交互链路进行测试
不能只测试“上传一张正常图片成功”。最低限度应覆盖:
空文件、零字节文件和超过大小限制的文件;
无扩展名、多个点、中文名、超长名和特殊字符文件;
MIME 类型与实际内容不一致;
未登录、越权访问和已删除文件;
同名文件重复上传;
预签名 URL 过期;
上传成功但数据库写入失败;
数据库记录成功但 OSS 上传失败;
Endpoint 与 Region 配置错误;
OSS 权限被撤销;
网络中断、超时和重复提交;
浏览器直传时的 CORS 预检;
移动端切后台、弱网和重新上传;
大文件分片缺失、重复和合并失败;
删除失败后的重试与最终状态;
生命周期规则是否误删或错误沉降文件。
前端显示“进度达到 100%”不一定意味着业务上传完成。对于分片上传,100% 可能只表示所有分片发送完毕;对于客户端直传,还需要完成 OSS 合并、业务确认和数据库状态更新。
一个可靠的完成条件应是:
OSS 已确认 Object 可用
并且
数据库中的文件状态为 READY
并且
用户重新进入页面后仍能访问该文件
七、可以直接交给 Cursor、Codex 或 Claude Code 的实现提示词
你是一名有生产环境经验的后端工程师。请在当前项目中接入阿里云对象存储 OSS,用于替代或完善现有的本地文件存储能力。
在修改代码前,请先完成以下检查:
1. 检查当前项目使用的语言、框架、构建工具、目录结构、配置管理方式和依赖版本。
2. 搜索项目中已有的文件上传、文件下载、本地目录、MultipartFile、静态资源映射、FileService、StorageService、附件表和用户头像逻辑。
3. 判断当前项目适合使用“后端中转上传”还是“客户端直传 OSS”。
4. 检查项目是否已经引入阿里云 OSS SDK、统一异常处理、配置中心、数据库迁移工具、任务队列和日志组件。
5. 先向我说明现有实现、存在的问题、推荐方案、数据流和需要修改的文件,不要立即开始堆代码。
目标行为:
- OSS Bucket 默认保持私有,不要通过公共读写规避权限设计。
- 不允许在前端、代码文件或 application.yml 中硬编码 AccessKey。
- 优先从环境变量、项目现有密钥系统或云上 RAM 角色读取凭证。
- Endpoint、Region 和 Bucket 必须通过配置管理,并校验三者是否匹配。
- 数据库保存 Bucket、Object Key、原始文件名、文件大小、Content-Type、所有者和状态,不保存长期带签名 URL。
- Object Key 不直接使用用户原始文件名,使用业务前缀、用户或租户 ID、日期和随机 ID 生成。
- 上传前校验登录状态、业务权限、文件大小、允许类型和文件数量。
- 不要把完整文件无必要地读取成 byte[],优先使用流式上传。
- 上传过程至少考虑 PENDING、READY、FAILED、DELETING、DELETED 状态。
- 处理“OSS 成功但数据库失败”和“数据库成功但 OSS 失败”的补偿与重试。
- 私有文件下载时,客户端提交业务 fileId,后端鉴权后再生成短期预签名 URL。
- 不允许客户端自由提交 Bucket 或任意 Object Key 生成下载地址。
- 如果使用客户端直传,只能返回受限、短期的 STS 凭证或预签名 URL,限制 Bucket、Object Key 前缀、操作方式和有效时间。
- 如果使用浏览器直传,正确配置并说明 CORS,但不要把 CORS 当成权限控制。
- 大文件根据现有业务规模评估是否使用分片上传和断点续传。
- 删除文件时使用可重试的状态流程,不假设 OSS 与数据库存在分布式事务。
- 复用项目已有依赖和基础设施,不随意引入重量级上传框架或重复的工具库。
- 将 OSS 客户端初始化、Object Key 生成、上传下载、业务权限和数据库记录拆分为职责清晰的模块。
- 不要在 Controller 中直接堆积 SDK 调用、数据库操作和权限判断。
- 日志中记录业务 requestId、fileId、Bucket、Object Key、耗时、错误码和 OSS Request ID,但不得输出任何密钥或完整临时凭证。
请先给出类似下面的数据流,并根据当前项目实际情况调整:
用户请求
→ 身份与业务权限校验
→ 文件参数校验
→ 创建 PENDING 文件记录
→ 生成 Object Key
→ 上传 OSS
→ 更新为 READY
→ 返回业务 fileId
下载流程:
用户提交 fileId
→ 查询文件记录
→ 验证访问权限
→ 获取 Object Key
→ 生成短期预签名 URL
→ 返回客户端
完成代码修改后,请说明:
1. 修改和新增了哪些文件。
2. 每个模块的职责。
3. 使用了哪些配置项和环境变量。
4. Object Key 的生成规则。
5. 文件状态如何变化。
6. 上传失败、数据库失败和删除失败如何处理。
7. 哪些参数可以调整,例如文件大小、签名有效期、允许类型、分片大小和并发数。
8. 如何在本地、测试环境和生产环境配置凭证。
9. 如何验证 RAM 最小权限是否正确。
10. 如何执行测试和回滚。
请检查以下场景:
- 正常上传、下载和删除;
- 空文件、超大文件、非法类型和伪造 Content-Type;
- 同名上传和重复提交;
- 未登录和越权访问;
- 签名 URL 过期;
- Endpoint、Region 或 Bucket 配置错误;
- OSS 上传成功但数据库写入失败;
- 数据库已有记录但 OSS 操作失败;
- 网络超时和重试;
- 大文件中断和续传;
- 浏览器 CORS 预检;
- 桌面端文件选择和拖拽;
- 触控板、鼠标、键盘操作不会触发重复提交;
- 移动端相册、文件选择、弱网、切后台和取消上传;
- 删除重试和孤儿 Object 清理;
- 应用关闭时 OSS 客户端资源是否正确释放。
不要把“接口返回 200”或“页面看起来可以上传”作为完成标准。最终必须验证:
- OSS 中确实存在正确的 Object;
- Object 的 Content-Type、大小和 Object Key 正确;
- 数据库记录与 OSS 状态一致;
- 无权限用户不能访问;
- 预签名 URL 过期后无法继续访问;
- 应用重启和多实例部署后文件仍然可用;
- 日志能够定位一次失败请求的具体原因。