阿里云 OSS 接入:Java SDK 上传、下载与权限配置
📝 笔记摘要:阿里云 OSS 的 Java 接入
OSS 是阿里云的对象存储服务,接口风格和 S3 一脉相承。接它真正要花时间的不是写代码,是把权限和密钥这件事摆平:用哪个账号、给哪把密钥、桶开成什么权限、URL 怎么发出去。这四件事里错任何一件,现象都是 403。
这篇按接入顺序走:地域和 endpoint 怎么选、RAM 用户怎么建、SDK 怎么初始化、文件怎么传上去、链接怎么发给别人、报错怎么查。代码用 aliyun-sdk-oss 3.18.5。
这篇的结构
1. OSS 的三个概念
2. 准备:RAM 用户和最小权限
3. SDK 依赖与客户端
4. 上传:流式 putObject
5. 桶的权限:私有桶加预签名 URL
6. 下载
7. 403 和 404 怎么查
8. 客户端是单例,密钥不进代码库
1. OSS 的三个概念
地域(Region) 桶必须建在某个地域里,endpoint 由地域决定。华东 1(杭州)是
oss-cn-hangzhou.aliyuncs.com
桶(Bucket) 一个隔离空间,权限、生命周期、版本控制都挂在桶上。桶名全局唯一,且只能是小写字母、数字和短横线
Object Key 对象在桶里的名字。它是扁平的,images/2026/10/a.jpg里的斜杠只是名字的一部分
两个 endpoint 后缀要分清:
oss-cn-hangzhou.aliyuncs.com 公网访问
oss-cn-hangzhou-internal.aliyuncs.com 同地域内网访问,走内部网络不收流量费,也不占公网带宽
应用部署在阿里云 ECS 上、桶也在同地域,就用内网 endpoint;应用在本地或者别的云上,用公网那个。填错了不是连不上,而是流量走公网、账单多一笔,很难第一时间联想到 endpoint。
对象访问地址的标准形式是:
https://{bucket}.{endpoint}/{objectKey}
2. 准备:RAM 用户和最小权限
最关键的一条:不要用主账号的 AccessKey 调应用接口。主账号的密钥拥有账号下所有资源的全部权限,一旦泄漏,删库删桶都是一次调用的事。
正确做法是建一个 RAM 子账号,只给它需要的那几个动作:
建 RAM 用户 控制台 → RAM → 用户 → 创建,类型选「OpenAPI 调用访问」,不要给控制台登录权限
绑定自定义策略 别直接给AliyunOSSFullAccess,自己写一条,限定桶名和动作
生成 AccessKey 在用户的「AccessKey」页签里创建,密钥只在创建时显示一次
用完就轮换 给第三方用的密钥单独建一个,定期换
一条够用的自定义策略长这样:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"oss:PutObject",
"oss:GetObject",
"oss:DeleteObject"
],
"Resource": [
"acs:oss:*:*:my-bucket",
"acs:oss:*:*:my-bucket/*"
]
}
]
}
提示:
Resource里必须同时写桶本身和桶里的对象两条。acs:oss:*:*:my-bucket管的是桶级操作(建桶、改策略),acs:oss:*:*:my-bucket/*管的是对象级操作(读写文件)。只写后一条的话,列桶会 403。
还有一件不能忘的事:密钥一旦出现在任何地方就要当作已泄漏。写进代码提交进 Git、发到群里、贴进文档截图,都算泄漏。发现泄漏后的动作是去 RAM 控制台禁用那把密钥并重新生成,而不是把代码里的那几行删掉就当没事发生——历史提交里还留着。
3. SDK 依赖与客户端
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.18.5</version>
</dependency>
构建客户端:
OSS ossClient = new OSSClientBuilder().build(
"https://" + endpoint,
System.getenv("OSS_ACCESS_KEY_ID"),
System.getenv("OSS_ACCESS_KEY_SECRET"));
配置走环境变量或者配置中心,代码里不出现密钥字面量。Spring Boot 项目里可以写成一个 @Configuration 里的单例 Bean:
@Configuration
public class OssConfig {
@Value("${oss.endpoint}")
private String endpoint;
@Value("${oss.accessKeyId}")
private String accessKeyId;
@Value("${oss.accessKeySecret}")
private String accessKeySecret;
@Bean
public OSS ossClient() {
return new OSSClientBuilder().build(
"https://" + endpoint, accessKeyId, accessKeySecret);
}
}
oss:
endpoint: oss-cn-hangzhou.aliyuncs.com
accessKeyId: ${OSS_ACCESS_KEY_ID}
accessKeySecret: ${OSS_ACCESS_KEY_SECRET}
开发环境把真实值放进不提交的 .env 或者 IDE 的运行配置里,仓库里只留带占位符的 application.yml。
提示:
OSSClient内部有连接池,建一个全局单例复用。每次请求 new 一个的话,连接和握手开销会成倍上去,高并发下连接数也容易打满。
4. 上传:流式 putObject
@RestController
@RequestMapping("/oss")
public class OssController {
private final OSS ossClient;
public OssController(OSS ossClient) {
this.ossClient = ossClient;
}
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(@RequestParam("file") MultipartFile file,
@RequestParam(required = false) String prefix) throws IOException {
String bucketName = "your-bucket";
// objectKey 里不能带桶名,也不能以 / 开头
String objectName = (prefix == null ? "" : prefix + "/") + file.getOriginalFilename();
try (InputStream inputStream = file.getInputStream()) {
ossClient.putObject(bucketName, objectName, inputStream);
}
return "https://" + bucketName + ".oss-cn-hangzhou.aliyuncs.com/" + objectName;
}
}
几个点值得单独说:
objectKey 不含桶名 写成
your-bucket/images/a.jpg会 404,桶名只在域名那一段
不以 / 开头 带前导斜杠的 key 会被当成另一层路径,实际访问不到
用流不用 byte[]getBytes()会把整个文件读进内存,OSS 的 SDK 支持直接传流
文件名要自己生成 原始文件名同样不可信,建议服务端重命名
想控制文件类型和大小,校验要在 OSS 之前做。OSS 侧的桶权限管的是「谁能访问」,不是「什么能存」。
大文件走分片上传(multipart upload),SDK 会自动处理分片和重试,接口和 putObject 差别不大,指定 UploadFileRequest 的分片大小和并发数即可。
5. 桶的权限:私有桶加预签名 URL
桶默认是私有的(private),这是对的。设置为公共读(public-read)确实能让图片链接直接打开,但代价是桶里所有对象对全网可读——包括你不小心传进去的合同、身份证、内部文档。
想让某个文件能被访问,又不想整个桶公开,办法是预签名 URL:桶保持私有,由服务端签一个带有效期的临时链接发给对方:
public String presign(String bucketName, String objectName, int expireSeconds) {
Date expiration = new Date(System.currentTimeMillis() + expireSeconds * 1000L);
URL url = ossClient.generatePresignedUrl(bucketName, objectName, expiration);
return url.toString();
}
链接在过期前能正常下载,过期就 403。要更短的时限就调小秒数,要给第三方传文件就用 generatePresignedPostPolicy,让对方直接 PUT 到 OSS,字节不经过你的应用。
提示:如果是公开的图片素材、静态资源,公共读的桶确实省事,但单独开一个桶,别和应用的其他文件混在一起。桶级别的粒度是安全边界,混了就没有了。
另外,「上传成功后把公网 URL 存进数据库」这个做法有个前提——桶必须是公共读或者 URL 是预签名的。私有桶存下来的那个 URL,外部谁也访问不了。
6. 下载
从 OSS 直接读流:
OSSObject ossObject = ossClient.getObject(bucketName, objectName);
try (InputStream is = ossObject.getObjectContent()) {
is.transferTo(response.getOutputStream());
} finally {
ossObject.close();
}
如果只是要把文件转给别人,不需要把字节搬一遍再吐出去,直接发预签名 URL,让浏览器自己去 OSS 拉。这条能省掉应用服务器的全部带宽和 CPU。
7. 403 和 404 怎么查
接 OSS 最费时间的就是排查这两个码。403 的成因有四种,按这个顺序查:
一、权限不够 RAM 用户的策略没给对应动作。先确认策略里有没有
oss:PutObject,再确认Resource写的桶名和实际桶名是否完全一致
二、桶策略挡了 桶的 Bucket Policy 或者防盗链规则(Referer 白名单)会额外拦截,Referer 不匹配也是 403
三、时钟偏差 报错是SignatureDoesNotMatch时,多半是机器时间不准。签名对时间敏感,差几分钟就失效,服务器记得开 NTP
四、endpoint 和桶地域不一致 请求打到了桶所在的另一个区域,服务端认为你没权限,于是 403
404 只有一种主因:objectKey 写错了。要么 key 拼错,要么把桶名写进了 key,要么 key 带了前导斜杠。用控制台的文件列表对一下实际的 key,长这样:
images/2026/10/photo.jpg
另外注意,OSS 对不存在的 key 和无权限的 key 返回同样的 404,这是刻意的设计,避免用错误码探测别人的桶里有什么。所以拿到 404 别只怀疑路径,权限问题伪装成 404 也很常见。
💡 补充:想省事可以用 ossutil 命令行工具排查,它是官方提供的,能直接看到完整的请求和错误详情,比看应用日志直观。配置方式和 SDK 一样,endpoint、AK、SK 三个参数。
🔗 同一套思路在别的存储上也成立:MinIO 自建对象存储 自建 S3 兼容存储的部署、纠删码和 Java 接入;Spring Boot 文件上传 本机磁盘那一档的完整实现,多副本和容量撑不住时就该换到 OSS;AList 容器部署 它支持挂 S3 后端,可以把 OSS 再包装成一个能预览的网盘。
🔥 一句话总结:接 OSS 的难点在权限不在代码,RAM 子账号 + 最小权限策略 + 私有桶 + 预签名 URL,这一套配齐了,剩下的就是把流传上去。
