阿里云 OSS 从零到实战:概念、配置与 Spring Boot 集成指南
一、OSS 核心概念
1.1 什么是 OSS
OSS(Object Storage Service,对象存储服务)是一种海量、安全、低成本的云存储服务。简单理解就是一个无限容量的网盘,通过 API 上传和下载文件。
与传统文件存储的区别:
| 维度 | 本地磁盘/NAS | OSS 对象存储 |
|---|---|---|
| 容量 | 有限,需要扩容 | 无限 |
| 访问方式 | 文件路径(/usr/local/file.txt) | HTTP URL |
| 可靠性 | 单机故障丢失 | 多副本冗余,99.9999999999% |
| 并发访问 | 有限 | 支持高并发读写 |
| CDN 加速 | 不支持 | 天然支持 |
| 成本 | 硬件+运维 | 按量付费 |
1.2 核心术语
Endpoint (接入点)
│
▼
Bucket (存储空间)
│
├── folder1/
│ ├── file1.xlsx
│ └── file2.pdf
│
└── folder2/
└── image.png
| 术语 | 说明 | 类比 |
|---|---|---|
| Endpoint | OSS 服务的访问域名,按地域区分 | 数据库的 host:port |
| Bucket | 存储空间,是文件的容器 | 数据库中的一个 database |
| Object | 存储的基本单元,即文件 | 数据库中的一行记录 |
| Object Key | 文件在 Bucket 中的唯一标识(路径+文件名) | 文件路径 |
| AccessKey ID / Secret | 访问凭证(身份认证) | 数据库的用户名/密码 |
| Region | 地域(如 cn-qingdao, cn-hangzhou) | 数据中心位置 |
1.3 Endpoint 格式
格式:<BucketName>.<Endpoint>
示例:xxx-img-xxx.oss-cn-xxxx.aliyuncs.com
内网访问:xxx-img-xxx.oss-cn-xxx-internal.aliyuncs.com(同地域免流量费)
1.4 Object Key(文件路径)
OSS 没有真正的文件夹概念,"文件夹"只是 Key 中的前缀:
upload/2026/06/10/abc123.xlsx ← 完整的 Object Key
├── upload/2026/06/10/ ← "文件夹"(实际是Key前缀)
└── abc123.xlsx ← "文件名"
1.5 访问权限
| 权限 | 说明 |
|---|---|
| private | 默认,需要签名 URL 才能访问 |
| public-read | 任何人可读,写入需认证 |
| public-read-write | 任何人可读写(不推荐) |
生产环境通常设为 private,程序通过 SDK 生成带签名的临时 URL 供用户下载。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、首次申请和开通 OSS
2.1 开通步骤
- 登录阿里云控制台 → 搜索"对象存储 OSS"
- 点击"立即开通"(按量付费,不用的时候几乎不花钱)
- 进入 OSS 管理控制台
2.2 创建 Bucket
- 点击"创建 Bucket"
- 填写信息:
- Bucket 名称 :全局唯一,如
my-app-files - 地域:选择离服务器最近的地域(如 cn-qingdao)
- 存储类型:标准存储(频繁访问)/ 低频访问 / 归档
- 读写权限:私有(推荐)
- 同城冗余:生产环境建议开启
- Bucket 名称 :全局唯一,如
- 点击确认创建
2.3 获取 AccessKey
- 鼠标悬停右上角头像 → "AccessKey 管理"
- 创建 AccessKey(会得到 AccessKey ID 和 AccessKey Secret)
- 重要:Secret 只显示一次,必须保存好
安全建议:不要使用主账号 AccessKey,创建 RAM 子账号并只授予 OSS 权限。
2.4 记录关键信息
创建完成后你会拥有:
Endpoint: oss-cn-xxx.aliyuncs.com
Bucket: my-app-files
AccessKey ID: xxxx
AccessKey Secret: xxxx
访问 URL 前缀: https://my-app-files.oss-cn-xxx.aliyuncs.com/
三、Spring Boot 集成 OSS
3.1 方式一:阿里云官方 SDK(通用)
依赖:
xml
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
3.3 配置(application.yml)
yaml
# 参考工程中的配置格式
jsh:
oss:
endpoint: oss-cn-xxx.aliyuncs.com
access-key-id: xx
access-key-secret: xxx
bucket-name: ylh-img-test
oss-url: https://image.ylhtest.com # CDN 域名或 Bucket 公网域名
max-size: 52480000 # 上传大小限制(50MB)
3.4 配置项含义详解
| 配置项 | 含义 | 示例 |
|---|---|---|
| endpoint | OSS 服务接入点域名 | oss-cn-qingdao.aliyuncs.com |
| access-key-id | 认证 ID | LTAI5tXXXX |
| access-key-secret | 认证密钥 | p9e27XXXX |
| bucket-name | 存储空间名称 | my-app-files |
| oss-url | 文件访问的基础 URL(可以是 CDN 域名) | https://image.example.com |
| max-size | 允许上传的最大文件大小(字节) | 52428800(50MB) |
四、完整示例:Spring Boot + 阿里云 OSS 文件上传下载
4.1 依赖
xml
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
4.2 配置
yaml
oss:
endpoint: oss-cn-xxx.aliyuncs.com
access-key-id: your-access-key-id
access-key-secret: your-access-key-secret
bucket-name: my-app-files
url-prefix: https://my-app-files.oss-cn-qingdao.aliyuncs.com
4.3 配置类
java
package com.example.config;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 阿里云 OSS 客户端配置.
*/
@Configuration
public class OssConfig {
@Value("${oss.endpoint}")
private String endpoint;
@Value("${oss.access-key-id}")
private String accessKeyId;
@Value("${oss.access-key-secret}")
private String accessKeySecret;
/**
* 创建 OSS 客户端 Bean.
* OSS 客户端是线程安全的,全局共享一个实例即可.
*/
@Bean(destroyMethod = "shutdown")
public OSS ossClient() {
return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
}
}
4.4 工具类
java
package com.example.util;
import com.aliyun.oss.OSS;
import com.aliyun.oss.model.OSSObject;
import com.aliyun.oss.model.PutObjectRequest;
import jakarta.annotation.Resource;
import java.io.File;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
/**
* OSS 文件操作工具类.
*/
@Slf4j
@Component
public class OssUtil {
@Resource
private OSS ossClient;
@Value("${oss.bucket-name}")
private String bucketName;
@Value("${oss.url-prefix}")
private String urlPrefix;
/**
* 上传 MultipartFile(Web 表单文件).
*
* @param file 上传的文件
* @param bizFolder 业务文件夹名(如 "import", "export")
* @return 文件访问 URL
*/
public String uploadMultipartFile(MultipartFile file, String bizFolder) {
String objectKey = generateObjectKey(bizFolder, getExtension(file.getOriginalFilename()));
try (InputStream is = file.getInputStream()) {
ossClient.putObject(bucketName, objectKey, is);
String url = urlPrefix + "/" + objectKey;
log.info("文件上传成功,objectKey:{}, url:{}", objectKey, url);
return url;
} catch (Exception e) {
log.error("文件上传失败", e);
throw new RuntimeException("文件上传失败", e);
}
}
/**
* 上传本地 File 对象.
*
* @param file 本地文件
* @param bizFolder 业务文件夹名
* @return 文件访问 URL
*/
public String uploadFile(File file, String bizFolder) {
String objectKey = generateObjectKey(bizFolder, getExtension(file.getName()));
PutObjectRequest request = new PutObjectRequest(bucketName, objectKey, file);
ossClient.putObject(request);
String url = urlPrefix + "/" + objectKey;
log.info("文件上传成功,objectKey:{}, url:{}", objectKey, url);
return url;
}
/**
* 上传字节流.
*
* @param inputStream 输入流
* @param bizFolder 业务文件夹名
* @param extension 文件扩展名(如 "xlsx")
* @return 文件访问 URL
*/
public String uploadStream(InputStream inputStream, String bizFolder, String extension) {
String objectKey = generateObjectKey(bizFolder, extension);
ossClient.putObject(bucketName, objectKey, inputStream);
String url = urlPrefix + "/" + objectKey;
log.info("文件上传成功,objectKey:{}, url:{}", objectKey, url);
return url;
}
/**
* 下载文件,返回输入流.
*
* @param objectKey 文件在 OSS 中的 Key
* @return 文件输入流(调用方需要关闭)
*/
public InputStream downloadFile(String objectKey) {
OSSObject ossObject = ossClient.getObject(bucketName, objectKey);
return ossObject.getObjectContent();
}
/**
* 通过完整 URL 下载文件.
*
* @param fileUrl 文件完整 URL
* @return 文件输入流
*/
public InputStream downloadByUrl(String fileUrl) {
// 从 URL 中提取 objectKey
String objectKey = fileUrl.replace(urlPrefix + "/", "");
return downloadFile(objectKey);
}
/**
* 删除文件.
*
* @param objectKey 文件在 OSS 中的 Key
*/
public void deleteFile(String objectKey) {
ossClient.deleteObject(bucketName, objectKey);
log.info("文件删除成功,objectKey:{}", objectKey);
}
/**
* 生成唯一的 Object Key.
* 格式:bizFolder/yyyy/MM/dd/uuid.extension
* 例如:import/2026/06/10/a1b2c3d4.xlsx
*/
private String generateObjectKey(String bizFolder, String extension) {
String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String fileName = UUID.randomUUID().toString().replace("-", "") + "." + extension;
return bizFolder + "/" + datePath + "/" + fileName;
}
/**
* 获取文件扩展名.
*/
private String getExtension(String filename) {
if (filename == null || !filename.contains(".")) {
return "unknown";
}
return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
}
}
4.5 业务使用示例
java
package com.example.service.impl;
import com.example.util.OssUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Service
public class FileBusinessService {
@Resource
private OssUtil ossUtil;
/**
* 场景一:接收用户上传的 Excel 文件,存到 OSS.
*/
public String handleFileUpload(MultipartFile file) {
// 直接上传到 OSS 的 "import" 文件夹下
return ossUtil.uploadMultipartFile(file, "import");
// 返回值如:https://my-app-files.oss-cn-xxx.aliyuncs.com/import/2026/06/10/xxx.xlsx
}
/**
* 场景二:程序生成 Excel 文件(如导入错误报告),上传到 OSS.
*/
public String generateAndUploadErrorReport(java.util.List<String> errors) {
File tempFile = null;
try {
// 1. 在内存中生成 Excel
XSSFWorkbook workbook = new XSSFWorkbook();
var sheet = workbook.createSheet("错误信息");
var headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("行号");
headerRow.createCell(1).setCellValue("错误原因");
for (int i = 0; i < errors.size(); i++) {
var row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(i + 1);
row.createCell(1).setCellValue(errors.get(i));
}
// 2. 写入临时文件
tempFile = File.createTempFile("error_report_", ".xlsx");
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
workbook.write(fos);
}
workbook.close();
// 3. 上传到 OSS
return ossUtil.uploadFile(tempFile, "error-reports");
} catch (Exception e) {
throw new RuntimeException("生成错误报告失败", e);
} finally {
// 4. 清理临时文件
if (tempFile != null && tempFile.exists()) {
tempFile.delete();
}
}
}
/**
* 场景三:从 OSS 下载文件提供给用户.
*/
public void downloadFromOss(String fileUrl, HttpServletResponse response) {
try (InputStream is = ossUtil.downloadByUrl(fileUrl)) {
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=download.xlsx");
byte[] buffer = new byte[8192];
int len;
var os = response.getOutputStream();
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
} catch (Exception e) {
throw new RuntimeException("文件下载失败", e);
}
}
}
五、OSS 常见使用场景
| 场景 | Object Key 设计 | 说明 |
|---|---|---|
| 用户上传的原始文件 | upload/原始文件名或UUID.xlsx |
保留用于异步处理 |
| 导入失败错误报告 | error-reports/2026/06/10/xxx.xlsx |
前端展示下载链接 |
| 导出的数据文件 | export/2026/06/10/xxx.xlsx |
大文件异步生成后通知用户下载 |
| 用户头像 | avatar/用户ID.jpg |
公开读,CDN 加速 |
| 合同/票据打印件 | contracts/2026/06/xxx.pdf |
私有,签名 URL 访问 |
六、生产环境最佳实践
6.1 安全
- 使用 RAM 子账号,只授予最小权限(如只能操作特定 Bucket)
- AccessKey 不硬编码在代码中,放在环境变量或配置中心(如 Nacos)
- Bucket 设为 private,通过后端接口代理访问或生成临时签名 URL
6.2 签名 URL(临时授权访问)
对于 private 文件,生成有时效的访问 URL:
java
// 生成有效期 1 小时的签名 URL
Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);
URL signedUrl = ossClient.generatePresignedUrl(bucketName, objectKey, expiration);
6.3 生命周期管理
配置自动清理规则,避免存储费用无限增长:
- 临时文件(error-reports/)7 天后自动删除
- 导出文件(export/)30 天后转低频存储,90 天后删除
- 在 OSS 控制台 → Bucket → 基础设置 → 生命周期 中配置
6.4 内网访问
如果应用服务器和 OSS 在同一地域,使用内网 Endpoint 可以:
- 免除流量费
- 速度更快、更稳定
yaml
# 公网
endpoint: oss-cn-qingdao.aliyuncs.com
# 内网(同地域 ECS 使用)
endpoint: oss-cn-qingdao-internal.aliyuncs.com
6.5 大文件处理
| 文件大小 | 推荐方式 |
|---|---|
| < 5MB | 普通上传(PutObject) |
| 5MB ~ 5GB | 分片上传(Multipart Upload) |
| > 5GB | 分片上传(必须) |
分片上传的好处:
- 断点续传
- 并行上传多个分片提速
- 单个分片失败只需重传该分片
七、费用说明
| 计费项 | 说明 | 大致费用 |
|---|---|---|
| 存储费 | 按存储容量计费 | 标准存储 ≈ 0.12 元/GB/月 |
| 流量费 | 外网下行流量 | ≈ 0.50 元/GB(内网免费) |
| 请求费 | PUT/GET 请求次数 | PUT 0.01 元/万次,GET 0.01 元/万次 |
| 数据取回费 | 低频/归档存储取回 | 低频 0.075 元/GB |
对于导入导出场景(文件存几天就删,主要走内网),费用非常低。
八、总结
| 步骤 | 操作 |
|---|---|
| 1. 开通 | 阿里云控制台开通 OSS 服务 |
| 2. 创建 Bucket | 选地域、命名、设权限 |
| 3. 获取凭证 | 创建 RAM 子账号,获取 AccessKey |
| 4. 引入依赖 | aliyun-sdk-oss 或公司封装库 |
| 5. 配置 | endpoint、bucket、accessKey 写入 YAML |
| 6. 编码 | 创建 OSSClient Bean → 封装工具类 → 业务调用 |
| 7. 安全 | 私有 Bucket + 签名 URL + RAM 最小权限 |
| 8. 运维 | 生命周期规则 + 内网访问 + 监控告警 |