Spring-Boot对接阿里云OSS从零到能跑
这篇是给 Spring Boot 初同学看的。我把对接阿里云 OSS 的整个过程拆开揉碎讲,包括原理、为什么这么做、key 怎么读取、那些文档里不写但你会踩的坑。对着这篇一步步来,不用再去翻别的资料。
背景:我手上有个 IM 项目(FriendChat),之前用的 MinIO(自己搭的对象存储),后来换成阿里云 OSS。这篇就以这次迁移为例,讲清楚怎么对接。
一、先说为啥要折腾这个
我一开始用 MinIO,自己 Docker 跑一个,挺美的——免费、兼容 S3 API、本地开发够用。但真上生产就几个问题:
- 得自己运维:MinIO 挂了文件全没,数据备份得自己搞,磁盘满了得自己扩
- 带宽是自己的:所有图片走应用服务器,用户一多服务器带宽就爆
- 没有 CDN 加速:外地用户访问慢
对象存储这玩意儿,云厂商已经做得又便宜又稳,真没必要自己搭。阿里云 OSS、腾讯云 COS、七牛云 Kodo,挑一个就行。我选 OSS 没别的原因,就是阿里云用的多、文档全。
所以这篇就是:Spring Boot 后端怎么把文件传到 OSS,怎么让前端能访问。
二、先把概念搞懂,别上来就写代码
很多人对接 OSS 一脸懵,是因为这几个概念没分清。我用大白话讲一遍。
2.1 Bucket(桶)
Bucket 就是装文件的容器。你可以理解成”一个顶层文件夹”。
- 名字全局唯一:整个阿里云所有用户里,你起的 Bucket 名不能和别人撞。比如
friendchat这种通用名早被占了,得起friendchat-wange-2026这种。 - 一个 Bucket 下可以放无数个文件,目录结构自己定。
- 权限策略是 Bucket 级别设的:私有(private)、公共读(public-read)、公共读写(public-read-write)。强烈建议私有,后面讲为啥。
2.2 Object(对象)
Object 就是一个文件。每个 Object 有:
key:文件的路径,比如image/2026/08/02/uuid.jpg。注意,OSS 没有真正的目录概念,这个 key 就是一整个字符串,斜杠只是看着像目录。你按image/2026/08/02/这样组织纯粹是为了管理方便。data:文件内容(二进制)。metadata:元数据,比如 Content-Type、文件大小。
2.3 Endpoint(接入点)
Endpoint 就是你访问 OSS 的域名。每个地域(region)有自己的 Endpoint,比如:
- 杭州:
oss-cn-hangzhou.aliyuncs.com - 上海:
oss-cn-shanghai.aliyuncs.com - 北京:
oss-cn-beijing.aliyuncs.com
重点:Endpoint 分外网和内网两种。
- 外网 Endpoint(
oss-cn-hangzhou.aliyuncs.com):走公网,谁都能访问,但要付公网流量费。 - 内网 Endpoint(
oss-cn-hangzhou-internal.aliyuncs.com):只有阿里云同地域的 ECS 能访问,免费!
如果你的后端部署在阿里云杭州的 ECS 上,一定要用内网 Endpoint,能省一大笔流量费。本地开发用外网。
2.4 Region(地域)
Region 是 Bucket 所在的物理机房位置。创建 Bucket 时选,选了就改不了。Bucket一旦创建,Region 就定了,所以一开始就想好:用户主要在哪 → 就选哪的 Region,延迟低。
2.5 AccessKey(访问密钥)
AccessKey 是你调用 OSS API 的身份凭证,分两个:
AccessKeyId:类似账号,可以明文传AccessKeySecret:类似密码,绝对不能泄露
签名算法用 AccessKeySecret 算签名,服务端验证。这个后面讲签名 URL 时会详细说。
安全提醒:绝对不要用主账号的 AccessKey! 万一泄露,你整个阿里云账号就裸奔了。一定要用 RAM 子账号,只给 OSS 的权限。后面讲怎么做。
三、开通 OSS:三步走
写代码前,先在阿里云控制台把东西准备好。
第 1 步:开通 OSS 服务
登录阿里云控制台,搜”对象存储 OSS”,进去点开通。开通不花钱,按用量计费。
第 2 步:创建 Bucket
点”创建 Bucket”,填几个东西:
- Bucket 名称:全局唯一,比如
friendchat-wange - 地域:选离你近的,比如华东1(杭州)
- 存储类型:选”标准存储”(频繁访问的文件用这个)
- 读写权限:选”私有”(重要!别选公共读)
创建完,你能看到一个 Bucket 的域名,类似 friendchat-wange.oss-cn-hangzhou.aliyuncs.com。记住这个,待会配置用。
为啥选私有?公共读意味着任何人拿到 URL 就能访问你的文件。IM 里的图片、文件是用户隐私,你不能保证 URL 不被泄露。私有 Bucket 下,文件 URL 必须带签名才能访问,签名还能设过期时间,泄露了也就那一段时间有效。安全得多。
第 3 步:创建 RAM 子账号拿 AccessKey
千万别用主账号 AccessKey! 正确做法:
- 控制台搜”RAM 访问控制”,进去
- 左边”用户”→”创建用户”
- 起个名字,比如
friendchat-oss,勾选”编程访问”(OpenAPI 调用),不要勾控制台访问(不需要登录控制台) - 创建完会显示 AccessKeyId 和 AccessKeySecret,这一页只显示一次,务必复制保存! 关了就再也看不到 Secret 了
- 给这个用户授权:权限管理里加
AliyunOSSFullAccess(或者更细粒度,只给某个 Bucket 的读写权限)
拿到 AccessKeyId 和 AccessKeySecret,待会写进配置文件。
四、开始写代码
准备工作做完了,开始写后端。我用的是 Spring Boot 2.7.3 + Java 8。
4.1 加依赖
打开 pom.xml,加阿里云 OSS 的 SDK:
1 | <dependency> |
为啥用这个版本? 3.17.x 是写这篇时的稳定版,支持 Java 8。如果你用的是 Java 9+,可以用更新的 3.18.x。版本可以在 mvnrepository 查。
注意: 如果你项目里之前用 MinIO,记得把 MinIO 的依赖删掉:
1 | <!-- 删掉这个 --> |
4.2 写配置
打开 src/main/resources/application.yml,加 OSS 配置:
1 | oss: |
几个要点讲清楚:
endpoint不要带http://或https://,SDK 会自己拼。你写oss-cn-hangzhou.aliyuncs.com就行。access-key-id和access-key-secret别硬编码进代码,放 yml 里。生产环境更推荐用环境变量或配置中心(Nacos),别把 Secret 提交到 Git!后面会讲怎么用环境变量。bucket-name写你创建的 Bucket 名,不带地域后缀。signed-url-expire是签名 URL 的有效期,3600 秒 = 1 小时。这个值看业务,IM 场景 1 小时够用(用户聊完天就关了)。如果是长会话可以设长点。
配置项名字的映射规则:yml 里我写的
access-key-id(kebab-case 短横线),Java 里字段叫accessKeyId(camelCase 驼峰)。Spring Boot 的@ConfigurationProperties支持松散绑定,会自动把access-key-id映射到accessKeyId。所以你写access-key-id、accessKeyId、access_key_id都行,但推荐 yml 用短横线,Java 用驼峰。
4.3 写配置类 OssConfig
新建 src/main/java/top/wange/config/OssConfig.java:
1 | package top.wange.config; |
这段代码每一行都讲清楚,因为初学者最容易在这里迷糊。
@Configuration 是啥
告诉 Spring:这个类是一个配置类,里面有 @Bean 方法。Spring 启动时会扫描这类,把 @Bean 方法的返回值注册成 Bean,放进 IoC 容器。
@ConfigurationProperties(prefix = "oss") 是啥
告诉 Spring:把 yml 里 oss 这个前缀下的配置项,按名字映射到这个类的字段上。流程是:
- Spring 启动,看到
@ConfigurationProperties(prefix = "oss") - 去 yml 里找
oss下的所有 key - 对每个 key,用反射找类里同名的 setter 方法,调用它注入值
@Data(Lombok)帮你生成了 setter,所以字段能被注入
1 | flowchart LR |
@Bean 是啥
@Bean 注解的方法,返回值会被注册为 Spring Bean。方法名 ossClient 就是 Bean 的名字。之后任何地方 @Autowired private OSS ossClient; 就能拿到这个实例。
1 | flowchart LR |
为啥 OSSClient 要做成单例 Bean
这是新手最容易犯的错。 OSSClient 不是个普通对象,它内部有:
- HTTP 连接池
- HTTP 客户端
- 线程池
创建一个 OSSClient 开销很大(要建连接池)。如果你每次上传都 new OSSClientBuilder().build(...),性能会差到爆炸,而且连接会泄漏。
好消息是 OSSClient 是线程安全的,多线程并发用一个实例没问题。所以整个应用共用一个,做成单例 Bean 最合适。
@PreDestroy 是啥
Bean 销毁前调用的回调方法。应用关闭时,Spring 会调 destroy(),关闭 OSSClient 的连接池,释放资源。不加这个的话,应用关闭时连接池里的连接不会被释放,虽然 JVM 退出会清,但不规范。
4.4 写上传服务
先定义接口 FileService:
1 | package top.wange.service; |
再写实现 FileServiceImpl:
1 | package top.wange.service.impl; |
这里有几个关键点必须讲:
objectName 怎么设计
objectName 就是文件在 OSS 里的路径。我用的格式是 fileType/yyyy/MM/dd/uuid.ext,比如 image/2026/08/02/550e8400-e29b-41d4-a716-446655440000.jpg。
为啥这么设计:
- 按类型分目录(
image/、file/、voice/、video/):方便统计、管理。比如想看图片占多少空间,直接看image/前缀。 - 按日期分目录(
2026/08/02/):虽然 OSS 单目录不限文件数,但按日期分方便排查问题(比如”8月2号上传的文件出问题了”)。 - 用 UUID 命名:避免同名文件冲突,也防止用户上传的文件名带特殊字符引发问题。不要用原始文件名存,中文、空格、特殊字符都是坑。
数据库存什么:objectName,不是 URL
这是个重要决策。我数据库 media_files 表的 file_path 字段存的是 objectName(相对路径),不是完整的 URL。
为啥?因为私有 Bucket 的 URL 是签名 URL,会过期。如果你存了完整 URL,1 小时后 URL 失效,数据库里这条记录就废了。存 objectName 的话,需要时用 generateSignedUrl(objectName) 现场生成一个新 URL,永远有效。
1 | flowchart TD |
ObjectMetadata 干啥用
1 | ObjectMetadata metadata = new ObjectMetadata(); |
setContentType:设文件的 MIME 类型,比如image/jpeg。这个很重要,浏览器访问文件时根据这个判断怎么处理。如果 Content-Type 是application/octet-stream(默认),浏览器会直接下载,而不是预览图片。setContentLength:设文件大小。OSS 上传时需要知道传多少字节,设上能避免一些边界问题。
try-with-resources 关闭流
1 | try (InputStream inputStream = file.getInputStream()) { |
MultipartFile.getInputStream() 拿到的流用完必须关。try-with-resources 语法会在 try 块结束时自动关闭流,比手动 finally 关干净。Java 7+ 都支持。
4.5 签名 URL 的原理(重点)
这块是私有 Bucket 对接的核心,讲透。
为啥需要签名 URL
私有 Bucket 的文件,你直接用 https://bucket.oss-cn-hangzhou.aliyuncs.com/image/xxx.jpg 访问,OSS 返回 403 Forbidden。因为 OSS 不知道你是谁,默认拒绝。
要让 OSS 认你,有两种方式:
- 请求头带签名:每次请求用 AccessKeySecret 算签名放 Header 里。适合后端直接调 OSS API。
- URL 带签名:把签名拼到 URL 的查询参数里,前端拿到 URL 直接 GET 就能访问。适合给前端用。
我们这里是给前端返回文件 URL,所以用方式2:签名 URL。
签名 URL 长啥样
1 | https://friendchat-wange.oss-cn-hangzhou.aliyuncs.com/image/2026/08/02/uuid.jpg |
三个查询参数:
OSSAccessKeyId:你的 AccessKeyId,告诉 OSS 你是谁Expires:过期时间(Unix 时间戳,秒),过了这个时间 URL 失效Signature:签名,用 AccessKeySecret 算出来的
签名怎么算的
签名算法是 HMAC-SHA1,大致流程:
1 | flowchart LR |
待签名字符串长这样(几个换行符拼接):
1 | GET\n\n\n1722567600\n/friendchat-wange/image/2026/08/02/uuid.jpg |
分别是:HTTP 方法、Content-MD5、Content-Type、Expires、资源路径。
重点理解:签名 URL 的安全性在于——Signature 是用 AccessKeySecret 算的,但 AccessKeySecret 从不出现在 URL 里。OSS 服务端收到请求,用它存的你的 AccessKeySecret,按同样算法算一遍签名,和你传的 Signature 对比,一致就放行。所以即使签名 URL 泄露,别人也推不出你的 AccessKeySecret,顶多在过期前能访问这个文件。
而且:签名 URL 一旦生成,后端就不参与访问了。前端直接 GET 这个 URL,OSS 直接返回文件,不经过你的后端。这就是对象存储省带宽的原理。
generatePresignedUrl 方法
1 | Date expiration = new Date(System.currentTimeMillis() + ossConfig.getSignedUrlExpire() * 1000L); |
HttpMethod.GET:生成的是 GET 请求的 URL(下载/访问用)。如果要前端直传(PUT),就用HttpMethod.PUT。setExpiration:设过期时间。- 返回的
URL对象toString()就是完整的签名 URL。
注意: ossConfig.getSignedUrlExpire() * 1000L 这里有个细节——必须乘 1000L 而不是 1000。因为 signedUrlExpire 是 long,乘 1000L 还是 long;如果乘 1000(int),在 long * int 时会自动提升,没问题,但养成加 L 的习惯,避免 int 溢出坑。
4.6 写 Controller
1 | package top.wange.controller; |
注意那个 /upload/url/{id} 接口,这是私有 Bucket 方案必备的。因为签名 URL 会过期,前端拿到一个图片 URL,1 小时后显示不出来,得有个接口重新换。前端检测到图片加载失败,就调这个接口拿新 URL。
五、私有 Bucket 的”坑”:头像 URL 会过期
这块是我迁移时踩的坑,文档里不写,但你不处理就完蛋。
问题
IM 项目里到处都有头像:
- 用户自己的头像
- 好友列表里每个好友的头像
- 群头像
- 会话列表里对方的头像
之前用 MinIO(公共读),头像 URL 存数据库,直接能访问,简单粗暴。换成私有 Bucket 后,数据库里存的如果是签名 URL,1 小时后所有头像全裂。
解决方案
数据库存 objectName,返回给前端前转签名 URL。
用户表 avatar_url 字段存的是 objectName(比如 image/2026/08/02/uuid.jpg),不是完整 URL。每次查询用户信息返回前,把 objectName 转成签名 URL:
1 | /** |
哪些地方要改
我项目里凡是返回头像给前端的地方都要改:
| 文件 | 改动点 |
|---|---|
| UserServiceImpl | getUserById、getUserByUsername、getOnlineUsers、searchUsers、login、updateUserProfile 返回前调 fillAvatarUrl |
| FriendServiceImpl | convertToFriendInfo、convertToPendingRequestInfo 里头像转签名 URL |
| MessageServiceImpl | 会话列表里群头像、对方头像、好友头像转签名 URL |
| UserInfo(Controller) | 上传头像时数据库存 objectName,返回前端用签名 URL |
有个统一的小工具方法,在每个 Service 里加一个:
1 | private String toSignedUrl(String url) { |
url.startsWith("http") 这个判断是兼容旧数据——MinIO 时代存的完整 URL(http 开头)原样返回,新数据(objectName)才转换。这样迁移时不用清洗历史数据。
上传头像时存什么
1 | // UserInfo.java 的上传头像接口 |
数据库存 objectName,前端拿签名 URL。这个区别一定要记住。
六、AccessKey 安全:别作死
这块必须单独说,太多人栽在这。
不要做的事
- 不要用主账号 AccessKey。主账号 AK 泄露 = 你整个阿里云账号被接管,人家能开机器、能删数据库、能扣你钱。
- 不要把 AccessKeySecret 提交到 Git。即使是私有仓库也不行,扫描器会扫 GitHub 上的 AK。
- 不要把 AK 写死在代码里。改一下要重新编译,多蠢。
正确做法
1. 用 RAM 子账号
前面开通那步已经讲了,建个 RAM 用户,只给 OSS 权限。这样即使泄露,也只能动 OSS,损失可控。
更细粒度:用 RAM 自定义策略,只允许操作某个 Bucket,不允许删除。这样更安全。
2. 用环境变量
application.yml 里用环境变量占位:
1 | oss: |
${OSS_ACCESS_KEY_ID} 的意思是:从环境变量 OSS_ACCESS_KEY_ID 读值。环境变量没设时,启动会报错(或者你可以给默认值 ${OSS_ACCESS_KEY_ID:默认值},但别这么干)。
部署时,环境变量在服务器上配(export OSS_ACCESS_KEY_ID=xxx),或者 Docker 用 -e,或者 K8s 用 Secret。
本地开发,可以在 IDEA 的运行配置里加 Environment Variables,或者在系统环境变量里加。
3. 开发环境用单独的子账号
给开发环境单独建个 RAM 子账号,只给测试 Bucket 的权限。生产环境另一个子账号,只有运维知道 Secret。这样开发人员拿不到生产 AK。
七、怎么测试
用 Postman 测上传
- 启动 Spring Boot 应用
- Postman 发 POST 请求
http://localhost:10086/api/upload - Headers 加
Authorization: Bearer <你的token> - Body 选
form-data,加两个:file:类型选 File,选个图片fileType:填IMAGE
- 发请求
成功的话返回:
1 | { |
把 url 复制到浏览器,能打开图片就成功了。
用 curl 测
1 | curl -X POST http://localhost:10086/api/upload \ |
验证签名 URL 过期
把 signed-url-expire 改成 60(60秒),重启,上传个文件拿到 URL,等 1 分钟再访问,应该返回 403 Forbidden(或者 404,看 OSS 版本)。说明过期机制生效。
八、常见报错和解决
1. com.aliyun.oss.OSSException: The Access Key Id you provided does not exist
原因:AccessKeyId 写错了,或者 RAM 用户被删了。
解决:去 RAM 控制台确认 AccessKeyId 对不对。
2. OSSException: SignatureDoesNotMatch
原因:AccessKeySecret 写错了。签名算出来和服务端不一致。
解决:重新去 RAM 复制 Secret。注意 Secret 复制时别多空格。
3. OSSException: AccessDenied. You have no right to access
原因:RAM 子账号没给 OSS 权限,或者 Bucket 读写策略限制。
解决:
- 给 RAM 用户加
AliyunOSSFullAccess权限 - 或者 Bucket 没创建好,去控制台确认 Bucket 名对不对
4. java.net.UnknownHostException: oss-cn-hangzhou.aliyuncs.com
原因:endpoint 写错了,或者网络不通。
解决:
- 确认 endpoint 拼写对(
oss-cn-hangzhou.aliyuncs.com,不带 http://) - 本地能不能 ping 通
- 公司网络可能拦了
5. 上传成功但签名 URL 访问 403
原因:Bucket 是私有的,但签名 URL 算错了,或者 objectName 对不上。
解决:
- 确认
generatePresignedUrl传的 objectName 和putObject的一致 - 确认 Bucket 名称对
- 看 OSS 控制台,文件是不是真传上去了
6. 上传图片后浏览器直接下载,不预览
原因:Content-Type 设错了,或者没设(默认 application/octet-stream)。
解决:确保 metadata.setContentType(file.getContentType()) 这行有,而且前端传的文件 MultipartFile 带了正确的 Content-Type。
7. Maven 下载不下来 aliyun-sdk-oss
原因:阿里云 SDK 在 Maven 中央仓库,但国内访问慢。
解决:pom.xml 里加阿里云镜像:
1 | <repositories> |
或者配 settings.xml 镜像(更通用)。
九、省钱和优化小技巧
1. 用内网 Endpoint 省流量费
这个前面强调了,生产环境后端部署在阿里云 ECS 上,一定要用内网 Endpoint(oss-cn-hangzhou-internal.aliyuncs.com)。
OSS 流量费分两种:
- 公网流量:贵,0.5 元/GB 左右
- 内网流量:免费
后端走内网传文件到 OSS 不花钱。前端访问签名 URL 是走公网(OSS 直连前端),这部分钱省不了,但后端→OSS 这段省了。
2. 配合 CDN 加速
如果你的用户全国都有,OSS 单地域访问慢。可以在 OSS 前面挂个 CDN:
- OSS 当源站
- CDN 回源到 OSS
- 用户访问 CDN 节点
CDN 流量比 OSS 公网流量便宜,而且全国节点加速。但这要改 URL 生成逻辑,签名方式也变(CDN 有自己的鉴权方式),这里不展开。
3. 生命周期规则
OSS 可以配生命周期规则,自动转储或删除旧文件。比如:
- 30 天前的文件转低频访问存储(便宜)
- 180 天前的转归档存储(更便宜)
- 365 天前的自动删除
适合日志、临时文件。IM 聊天文件一般不删,可以不配。
4. 防盗链
OSS Bucket 可以配 Referer 白名单,只允许从你的域名访问。防止别人盗用你的图片链接。但和签名 URL 一起用要注意,签名 URL 是 GET 请求,Referer 防盗链能加一层保护。
5. 分片上传
大于 5MB 的文件,用分片上传(MultipartUpload)更稳:网络断了能续传,大文件不会因为超时失败。SDK 有 UploadFileRequest 封装好了。我项目里视频最大 50MB,普通上传够用,没做分片。如果你要传几百 M 的,一定要分片。
十、整个流程串起来
最后用一张图把整个流程串一遍,你脑子里有这个图就全懂了:
1 | sequenceDiagram |
写在最后
对接 OSS 本身不难,难的是把整个链路想清楚:
- 存储:objectName 怎么设计,数据库存什么
- 访问:私有 Bucket 用签名 URL,URL 会过期怎么办
- 安全:AccessKey 怎么管,别用主账号
- 成本:内网 Endpoint 省钱,CDN 加速
这篇讲的是服务端直传(前端→后端→OSS),简单但占后端带宽。如果你的文件很大(比如几百 M 视频),建议改客户端直传:前端拿 STS 临时凭证直接传 OSS,后端只发凭证不传文件。那个方案更复杂,等你有需要再研究。
代码我项目里都跑通了,对着抄就行。有不清楚的地方多看几遍原理部分,理解了原理,换个云厂商(腾讯云 COS、七牛)也是一样的套路。
—— 完

