Spring Boot 集成阿里云 OSS 实现文件上传下载的完整指南(从概念到代码)
Spring Boot 集成阿里云 OSS 实现文件上传下载的完整指南(从概念到代码)
一、什么是 OSS
OSS(Object Storage Service,对象存储服务)是云厂商提供的海量文件存储服务。你可以把它理解为一个无限容量的网盘 API,通过代码上传/下载/删除文件。
类比理解:
| 概念 | 类比 | 说明 |
|---|---|---|
| OSS 服务 | 网盘(百度网盘) | 存文件的地方 |
| Bucket(存储桶) | 网盘里的根文件夹 | 一个项目通常对应一个 Bucket |
| Object(对象) | 网盘里的文件 | 每个上传的文件就是一个 Object |
| Object Key | 文件路径 | 如images/2024/avatar.jpg |
| Endpoint(节点) | 服务器地址 | 如oss-cn-qingdao.aliyuncs.com |
| AccessKey | 账号密码 | 用于身份认证 |
为什么不直接存本地服务器:
| 对比项 | 本地存储 | OSS |
|---|---|---|
| 容量 | 受限于磁盘大小 | 无限 |
| 可靠性 | 服务器挂了文件丢失 | 99.9999999% 可靠性 |
| 访问速度 | 单机带宽瓶颈 | CDN 加速,全球访问 |
| 多实例部署 | 文件只在一台机器上 | 所有实例都能访问 |
| 成本 | 需要买大磁盘 | 按量付费,用多少算多少 |
二、核心概念详解
1. Bucket(存储桶)
一个 Bucket = 一个独立的文件命名空间 例如: my-edu-platform(教育平台的文件桶) ├── avatars/user001.jpg ├── courses/math/chapter1.pdf └── exports/report_2024.xlsxBucket 创建后有一个访问域名:https://my-edu-platform.oss-cn-qingdao.aliyuncs.com
2. Object Key(文件路径)
OSS 没有真正的"文件夹"概念,/只是 Key 的一部分:
avatars/user001.jpg → 这是一个完整的 Object Key courses/math/chapter1.pdf → 这也是一个完整的 Object Key3. 访问权限
| 权限 | 说明 | 适用场景 |
|---|---|---|
| private | 必须签名才能访问 | 用户隐私文件、付费内容 |
| public-read | 任何人可读 | 头像、公开图片 |
| public-read-write | 任何人可读写 | 几乎不用(不安全) |
4. 签名 URL(临时访问链接)
对于 private 文件,可以生成一个带过期时间的临时访问链接:
https://my-bucket.oss-cn-qingdao.aliyuncs.com/secret/file.pdf ?OSSAccessKeyId=xxx &Expires=1704067200 &Signature=abc123过期后链接自动失效,适合付费下载、临时分享等场景。
三、Maven 依赖
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.15.1</version></dependency>四、完整示例:个人博客图片管理系统
场景
一个个人博客系统,用户可以:
- 上传文章配图
- 获取图片访问链接
- 删除不需要的图片
- 批量上传多张图片
1. 配置文件 application.yml
aliyun:oss:endpoint:oss-cn-qingdao.aliyuncs.comaccess-key-id:your-access-key-idaccess-key-secret:your-access-key-secretbucket-name:my-blog-images# 文件访问域名前缀(如果绑定了自定义域名)url-prefix:https://my-blog-images.oss-cn-qingdao.aliyuncs.com2. OSS 配置类
packagecom.example.config;importcom.aliyun.oss.OSS;importcom.aliyun.oss.OSSClientBuilder;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;@ConfigurationpublicclassOssConfig{@Value("${aliyun.oss.endpoint}")privateStringendpoint;@Value("${aliyun.oss.access-key-id}")privateStringaccessKeyId;@Value("${aliyun.oss.access-key-secret}")privateStringaccessKeySecret;/** * 创建 OSS 客户端 Bean * 整个应用共享一个实例(线程安全) */@BeanpublicOSSossClient(){returnnewOSSClientBuilder().build(endpoint,accessKeyId,accessKeySecret);}}3. OSS 工具服务类
packagecom.example.service;importcom.aliyun.oss.OSS;importcom.aliyun.oss.model.ObjectMetadata;importcom.aliyun.oss.model.PutObjectRequest;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.stereotype.Service;importorg.springframework.web.multipart.MultipartFile;importjava.io.InputStream;importjava.net.URL;importjava.util.Date;importjava.util.UUID;@Slf4j@ServicepublicclassOssService{privatefinalOSSossClient;@Value("${aliyun.oss.bucket-name}")privateStringbucketName;@Value("${aliyun.oss.url-prefix}")privateStringurlPrefix;publicOssService(OSSossClient){this.ossClient=ossClient;}/** * 上传文件 * * @param file 前端上传的文件 * @param directory 存储目录(如 "articles/2024/06") * @return 文件的访问URL */publicStringuploadFile(MultipartFilefile,Stringdirectory){// 1. 生成唯一文件名(防止重名覆盖)StringoriginalFilename=file.getOriginalFilename();Stringextension=originalFilename.substring(originalFilename.lastIndexOf("."));StringobjectKey=directory+"/"+UUID.randomUUID().toString().replace("-","")+extension;// 例如:articles/2024/06/a1b2c3d4e5f6.jpgtry{// 2. 设置文件元信息ObjectMetadatametadata=newObjectMetadata();metadata.setContentType(file.getContentType());metadata.setContentLength(file.getSize());// 3. 上传到 OSSInputStreaminputStream=file.getInputStream();PutObjectRequestputRequest=newPutObjectRequest(bucketName,objectKey,inputStream,metadata);ossClient.putObject(putRequest);// 4. 返回访问URLStringfileUrl=urlPrefix+"/"+objectKey;log.info("文件上传成功, objectKey={}, url={}",objectKey,fileUrl);returnfileUrl;}catch(Exceptione){log.error("文件上传失败, fileName={}, error={}",originalFilename,e.getMessage());thrownewRuntimeException("文件上传失败:"+e.getMessage());}}/** * 生成临时访问链接(适用于私有文件) * * @param objectKey 文件路径 * @param expireMinutes 过期时间(分钟) * @return 带签名的临时URL */publicStringgeneratePresignedUrl(StringobjectKey,intexpireMinutes){// 设置过期时间Dateexpiration=newDate(System.currentTimeMillis()+expireMinutes*60*1000L);// 生成签名URLURLurl=ossClient.generatePresignedUrl(bucketName,objectKey,expiration);returnurl.toString();}/** * 删除文件 * * @param objectKey 文件路径 */publicvoiddeleteFile(StringobjectKey){try{ossClient.deleteObject(bucketName,objectKey);log.info("文件删除成功, objectKey={}",objectKey);}catch(Exceptione){log.error("文件删除失败, objectKey={}, error={}",objectKey,e.getMessage());thrownewRuntimeException("文件删除失败:"+e.getMessage());}}/** * 判断文件是否存在 */publicbooleandoesFileExist(StringobjectKey){returnossClient.doesObjectExist(bucketName,objectKey);}/** * 从完整URL中提取objectKey * 例如:https://my-blog-images.oss-cn-qingdao.aliyuncs.com/articles/2024/06/abc.jpg * 提取为:articles/2024/06/abc.jpg */publicStringextractObjectKey(StringfileUrl){returnfileUrl.replace(urlPrefix+"/","");}}4. Controller
packagecom.example.controller;importcom.example.service.OssService;importio.swagger.annotations.Api;importio.swagger.annotations.ApiOperation;importorg.springframework.web.bind.annotation.*;importorg.springframework.web.multipart.MultipartFile;importjava.time.LocalDate;importjava.time.format.DateTimeFormatter;@Api(tags="图片管理")@RestController@RequestMapping("/image")publicclassImageController{privatefinalOssServiceossService;publicImageController(OssServiceossService){this.ossService=ossService;}/** * 上传图片 * 前端用 form-data 格式提交,字段名为 file */@ApiOperation("上传图片")@PostMapping("/upload")publicR<String>upload(@RequestParam("file")MultipartFilefile){// 按日期分目录存储:articles/2024/06/xxx.jpgStringdirectory="articles/"+LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));Stringurl=ossService.uploadFile(file,directory);returnnewR<>(url);}/** * 获取私有文件的临时访问链接 */@ApiOperation("获取临时访问链接")@GetMapping("/presignedUrl")publicR<String>getPresignedUrl(@RequestParam("objectKey")StringobjectKey){Stringurl=ossService.generatePresignedUrl(objectKey,30);// 30分钟有效returnnewR<>(url);}/** * 删除图片 */@ApiOperation("删除图片")@DeleteMapping("/delete")publicR<String>delete(@RequestParam("fileUrl")StringfileUrl){StringobjectKey=ossService.extractObjectKey(fileUrl);ossService.deleteFile(objectKey);returnnewR<>("删除成功");}}五、执行过程演示
上传文件
前端:POST /image/upload (form-data, file=photo.jpg) 后端处理: 1. 生成 objectKey = "articles/2024/06/a1b2c3d4e5f6.jpg" 2. 调用 ossClient.putObject(bucketName, objectKey, inputStream, metadata) 3. OSS 存储文件,返回成功 4. 拼接访问URL = "https://my-blog-images.oss-cn-qingdao.aliyuncs.com/articles/2024/06/a1b2c3d4e5f6.jpg" 返回给前端: { "code": "1", "data": "https://my-blog-images.oss-cn-qingdao.aliyuncs.com/articles/2024/06/a1b2c3d4e5f6.jpg" } 前端拿到URL后插入到文章的 <img src="..."> 中生成临时链接
请求:GET /image/presignedUrl?objectKey=private/report.pdf 后端处理: 1. 设置过期时间 = 当前时间 + 30分钟 2. 调用 ossClient.generatePresignedUrl(bucket, key, expiration) 3. OSS SDK 本地计算签名(不需要网络请求) 返回: "https://my-blog-images.oss-cn-qingdao.aliyuncs.com/private/report.pdf ?OSSAccessKeyId=LTAI5t*** &Expires=1704069000 &Signature=abc123def456" 30分钟后这个链接自动失效,无法再访问六、OSS 存储结构示例
Bucket: my-blog-images │ ├── articles/ ← 文章配图 │ ├── 2024/ │ │ ├── 05/ │ │ │ ├── a1b2c3.jpg │ │ │ └── d4e5f6.png │ │ └── 06/ │ │ └── g7h8i9.jpg │ └── 2025/ │ └── ... │ ├── avatars/ ← 用户头像 │ ├── user001.jpg │ └── user002.png │ └── exports/ ← 导出文件 ├── report_20240601.xlsx └── task_xxx.zip七、常见使用模式
模式 1:公开文件(头像、文章图片)
// 上传后直接返回公开URL,任何人可访问Stringurl="https://bucket.oss-cn-qingdao.aliyuncs.com/avatars/user001.jpg";模式 2:私有文件 + 临时链接(付费内容、敏感文件)
// 上传时设为私有,访问时生成临时签名URLStringpresignedUrl=ossService.generatePresignedUrl("private/vip-course.mp4",60);// 返回给前端,60分钟内有效模式 3:服务端直传(大文件)
// 后端生成上传凭证,前端直接传到OSS,不经过后端服务器// 适合大文件(视频、大型PDF),避免占用后端带宽模式 4:回调通知
前端上传到OSS → OSS上传完成后回调后端接口 → 后端记录文件信息到数据库八、注意事项
| 事项 | 说明 |
|---|---|
| AccessKey 安全 | 绝不能暴露在前端代码或 Git 仓库中 |
| 文件名唯一 | 用 UUID 生成,避免覆盖 |
| 目录规划 | 按业务/日期分目录,便于管理和清理 |
| 文件大小限制 | 普通上传最大 5GB,超过用分片上传 |
| 跨域配置 | 前端直传需要在 OSS 控制台配置 CORS |
| 费用 | 按存储量 + 请求次数 + 流量计费,注意监控 |
九、总结
OSS 的本质就是:通过 API 操作的无限容量文件系统。
核心操作只有四个:
ossClient.putObject(bucket,key,inputStream);// 上传ossClient.getObject(bucket,key);// 下载ossClient.deleteObject(bucket,key);// 删除ossClient.generatePresignedUrl(bucket,key,expire);// 生成临时链接在项目中的典型用法:
- 用户上传文件 → 存到 OSS → 数据库只存 URL/objectKey
- 用户访问文件 → 公开文件直接用 URL / 私有文件生成临时链接
- 异步任务生成文件 → 存到 OSS → 返回下载链接给前端
