1
0
0

阿里云 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,这一套配齐了,剩下的就是把流传上去。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!