Spring Boot 集成阿里云 OSS 实现文件上传下载的完整指南从概念到代码一、什么是 OSSOSSObject Storage Service对象存储服务是云厂商提供的海量文件存储服务。你可以把它理解为一个无限容量的网盘 API通过代码上传/下载/删除文件。类比理解概念类比说明OSS 服务网盘百度网盘存文件的地方Bucket存储桶网盘里的根文件夹一个项目通常对应一个 BucketObject对象网盘里的文件每个上传的文件就是一个 ObjectObject Key文件路径如images/2024/avatar.jpgEndpoint节点服务器地址如oss-cn-qingdao.aliyuncs.comAccessKey账号密码用于身份认证为什么不直接存本地服务器对比项本地存储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.com2. 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 ?OSSAccessKeyIdxxx Expires1704067200 Signatureabc123过期后链接自动失效适合付费下载、临时分享等场景。三、Maven 依赖dependencygroupIdcom.aliyun.oss/groupIdartifactIdaliyun-sdk-oss/artifactIdversion3.15.1/version/dependency四、完整示例个人博客图片管理系统场景一个个人博客系统用户可以上传文章配图获取图片访问链接删除不需要的图片批量上传多张图片1. 配置文件 application.ymlaliyun: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;Slf4jServicepublicclassOssService{privatefinalOSSossClient;Value(${aliyun.oss.bucket-name})privateStringbucketName;Value(${aliyun.oss.url-prefix})privateStringurlPrefix;publicOssService(OSSossClient){this.ossClientossClient;}/** * 上传文件 * * param file 前端上传的文件 * param directory 存储目录如 articles/2024/06 * return 文件的访问URL */publicStringuploadFile(MultipartFilefile,Stringdirectory){// 1. 生成唯一文件名防止重名覆盖StringoriginalFilenamefile.getOriginalFilename();StringextensionoriginalFilename.substring(originalFilename.lastIndexOf(.));StringobjectKeydirectory/UUID.randomUUID().toString().replace(-,)extension;// 例如articles/2024/06/a1b2c3d4e5f6.jpgtry{// 2. 设置文件元信息ObjectMetadatametadatanewObjectMetadata();metadata.setContentType(file.getContentType());metadata.setContentLength(file.getSize());// 3. 上传到 OSSInputStreaminputStreamfile.getInputStream();PutObjectRequestputRequestnewPutObjectRequest(bucketName,objectKey,inputStream,metadata);ossClient.putObject(putRequest);// 4. 返回访问URLStringfileUrlurlPrefix/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){// 设置过期时间DateexpirationnewDate(System.currentTimeMillis()expireMinutes*60*1000L);// 生成签名URLURLurlossClient.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. Controllerpackagecom.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图片管理)RestControllerRequestMapping(/image)publicclassImageController{privatefinalOssServiceossService;publicImageController(OssServiceossService){this.ossServiceossService;}/** * 上传图片 * 前端用 form-data 格式提交字段名为 file */ApiOperation(上传图片)PostMapping(/upload)publicRStringupload(RequestParam(file)MultipartFilefile){// 按日期分目录存储articles/2024/06/xxx.jpgStringdirectoryarticles/LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM));StringurlossService.uploadFile(file,directory);returnnewR(url);}/** * 获取私有文件的临时访问链接 */ApiOperation(获取临时访问链接)GetMapping(/presignedUrl)publicRStringgetPresignedUrl(RequestParam(objectKey)StringobjectKey){StringurlossService.generatePresignedUrl(objectKey,30);// 30分钟有效returnnewR(url);}/** * 删除图片 */ApiOperation(删除图片)DeleteMapping(/delete)publicRStringdelete(RequestParam(fileUrl)StringfileUrl){StringobjectKeyossService.extractObjectKey(fileUrl);ossService.deleteFile(objectKey);returnnewR(删除成功);}}五、执行过程演示上传文件前端POST /image/upload form-data, filephoto.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?objectKeyprivate/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 ?OSSAccessKeyIdLTAI5t*** Expires1704069000 Signatureabc123def456 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任何人可访问Stringurlhttps://bucket.oss-cn-qingdao.aliyuncs.com/avatars/user001.jpg;模式 2私有文件 临时链接付费内容、敏感文件// 上传时设为私有访问时生成临时签名URLStringpresignedUrlossService.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 → 返回下载链接给前端