Java 对接阿里云 OSS 私有读写(开箱即用)

在现代应用开发中,对象存储服务(OSS)是处理图片、视频、文档等非结构化数据的常用方式。阿里云 OSS(Object Storage Service) 提供了高可靠、高可用的对象存储服务,适用于各种企业级应用场景。

本文将详细介绍如何使用 Java SDK 对接阿里云 OSS,并实现私有读写的操作。内容包括:

  • 阿里云 OSS 简介
  • 为什么选择私有 Bucket?
  • ACL 权限详解:Public Read/Write vs Private
  • 准备工作:开通服务与获取密钥
  • Maven 依赖配置
  • Java 实现上传、下载、删除文件
  • 使用签名 URL 实现私有访问(重点讲解原理和优势)
  • 安全建议与最佳实践

🔍 一、阿里云 OSS 简介

阿里云 OSS 是一个海量、安全、低成本、高可靠的云端对象存储服务。支持任意形式的文件上传和下载,广泛用于网站托管、图片资源管理、日志存储、大数据分析等领域。

默认情况下,OSS 的 Bucket 权限为私有(Private),即只有授权用户可以访问。本文重点介绍如何在 Java 应用中对接私有 Bucket,实现文件的安全上传与受控访问。


🧩 二、为何要选择“私有读写”?

1. 什么是 ACL?

ACL(Access Control List)是阿里云 OSS 中用于控制 Bucket 和 Object 访问权限的一种机制。常见的类型包括:

类型描述
private默认值,仅 Bucket 拥有者可读写
public-read所有人可读,Bucket 拥有者可写
public-read-write所有人可读写(非常不推荐)

2. 为什么要使用 private

  • 安全性高:只有通过授权(如 AccessKey 或签名 URL)才能访问。
  • ❗️避免误公开:防止敏感文件被外网直接访问。
  • ✅ 控制精细:适合需要身份验证或临时授权访问的场景。
  • ⚠️ 不适合开放资源:如果你希望某些文件对外完全公开,请单独设置签名链接或 CDN 加速。

3. 签名 URL 的好处是什么?

由于私有 Bucket 不允许外部直接访问,我们需要一种机制让特定用户在限定时间内访问文件。这时就用到了 签名 URL(Presigned URL)

特点如下:
特性描述
时效性可设置过期时间,比如 5 分钟后失效
权限控制只能执行指定的操作(GET / PUT)
无需认证用户不需要拥有阿里云账号即可访问
灵活分享可用于前端预览、邮件附件下载等场景

📌 总结:签名 URL 是实现私有访问的关键手段,既能保证安全性,又能满足临时访问需求。


⚙️ 三、准备工作

1. 开通阿里云 OSS 服务

前往 阿里云控制台 注册账号并开通 OSS 服务。

2. 创建 Bucket

进入 OSS 控制台 → 新建 Bucket

  • 设置区域(Region)
  • 设置 Bucket 名称
  • 访问权限选择 私有(Private)

3. 获取 AccessKey

进入 RAM 控制台 → 用户管理 → 创建子用户

  • 勾选“编程访问”
  • 授权 AliyunOSSFullAccess 权限(或根据需求自定义权限)
  • 保存 AccessKey ID 和 Secret

4. 设置oss存储桶

在这里插入图片描述
在这里插入图片描述
设置了为私有读写权限后 按照下面代码就可以直接调用成功了

✅ 安全提示:不要将 AccessKey 暴露在前端或公共仓库中,建议使用环境变量或配置中心管理。


📦 四、Maven 依赖配置

<dependencies>
    <!-- 阿里云 OSS Java SDK -->
    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
        <version>3.17.4</version>
    </dependency>
</dependencies>

💻 五、Java 实现基本操作

1. 创建AliyunOSSService 服务类 通过sdk上传图片和加密图片



import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.CredentialsProvider;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.comm.SignVersion;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectRequest;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.UUID;

/**
 * @Author: suhai
 * @Date: 2025/6/5
 * @Description: 使用阿里云 OSS SDK V4.x 的上传服务 私有读写上传文件图片
 */
@Service
public class AliyunOSSService {

    // todo 后续换成 config配置文件获取
    private String endpoint = "https://oss-cn-chengdu.aliyuncs.com";
    private String bucketName = "menstrual-back";//桶名
    private String region = "cn-chengdu"; //
    private String maxFileSize = "10MB";
    String accessKeyId = "";
    String accessKeySecret = "";
    /**
     * 上传文件到OSS
     * @param file 文件
     * @param folder 上传到的文件夹
     * @return 文件访问URL
     */
    public String uploadFile(MultipartFile file, String folder) {
        validateFile(file);
        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
        CredentialsProvider credentialsProvider = CredentialsProviderFactory.newDefaultCredentialProvider(accessKeyId,accessKeySecret);
        OSS ossClient = OSSClientBuilder.create()
                .endpoint(endpoint)
                .credentialsProvider(credentialsProvider)
                .clientConfiguration(clientBuilderConfiguration)
                .region(region)
                .build();
        try {
            String originalFilename = file.getOriginalFilename();
            String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
            String safeFileName = UUID.randomUUID() + fileExtension;

            // URL 编码,防止非法字符
            String encodedFileName = URLEncoder.encode(safeFileName, StandardCharsets.UTF_8.toString()).replace("+", "%20");

            String filePath = folder + "/" + encodedFileName;

            InputStream inputStream = file.getInputStream();

            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, filePath, inputStream);
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setHeader("Content-Type", file.getContentType());
            putObjectRequest.setMetadata(metadata);
            ossClient.putObject(putObjectRequest);
            return generateFileUrl(filePath);
        } catch (OSSException oe) {
            throw new RuntimeException("OSS错误 - 上传失败: " + oe.getErrorMessage(), oe);
        } catch (Exception e) {
            throw new RuntimeException("上传失败: " + e.getMessage(), e);
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }

    /**
     * 验证文件是否符合要求
     */
    private void validateFile(MultipartFile file) {
        long maxSize = parseSize(maxFileSize);
        if (file.getSize() > maxSize) {
            throw new RuntimeException("文件大小不能超过 " + maxFileSize);
        }

        String originalFilename = file.getOriginalFilename();
        if (originalFilename == null || !originalFilename.contains(".")) {
            throw new RuntimeException("无效的文件名");
        }

        String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toLowerCase();

        List<String> allowedExtensions = Arrays.asList("jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx".split(","));
        if (!allowedExtensions.contains(fileExtension)) {
            throw new RuntimeException("不支持的文件类型,仅支持: " + String.join(",", allowedExtensions));
        }
    }

    /**
     * 生成文件访问URL
     */
    private String generateFileUrl(String filePath) {
        return endpoint.replace("https://", "https://" + bucketName + ".") + "/" + filePath;
    }

    /**
     * 将字符串格式的文件大小转换为字节数
     */
    private long parseSize(String size) {
        size = size.toUpperCase();
        if (size.endsWith("KB")) {
            return Long.parseLong(size.substring(0, size.length() - 2)) * 1024;
        } else if (size.endsWith("MB")) {
            return Long.parseLong(size.substring(0, size.length() - 2)) * 1024 * 1024;
        } else if (size.endsWith("GB")) {
            return Long.parseLong(size.substring(0, size.length() - 2)) * 1024 * 1024 * 1024;
        } else {
            return Long.parseLong(size);
        }
    }


    //私有读加签名
    public String getPrivateReadSignatureUrl(String filePath) throws MalformedURLException {
        URL urlbase = new URL(filePath);
        String objetName = urlbase.getPath();
        if (objetName.startsWith("/")) {
            objetName = objetName.substring(1);
        }
        CredentialsProvider credentialsProvider = CredentialsProviderFactory.newDefaultCredentialProvider(accessKeyId,accessKeySecret);
        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
        OSS ossClient = OSSClientBuilder.create()
                .endpoint(endpoint)
                .credentialsProvider(credentialsProvider)
                .clientConfiguration(clientBuilderConfiguration)
                .region(region)
                .build();

        try {
            Date expiration = new Date(new Date().getTime() + 3600 * 1000L);
            // 生成以GET方法访问的预签名URL。本示例没有额外请求头,其他人可以直接通过浏览器访问相关内容。
            URL url = ossClient.generatePresignedUrl(bucketName, objetName, expiration);
            return url.toString();
        } catch (OSSException oe) {
            throw new RuntimeException("OSS错误 - 上传失败: " + oe.getErrorMessage(), oe);
        } catch (ClientException ce) {
            throw new RuntimeException("客户端错误 - 上传失败: " + ce.getErrorMessage(), ce);
        } finally {
            ossClient.shutdown();
        }
    }
}


2. 编写controller "temp"是需要上传的目录路径 因为是私有读 在上传图片返回的路径后需要给前端签名后的地址 所以还需要调用签名

/**
 * 文件请求处理
 *
 * @author suhai
 */
@RestController
public class OssFileUploadController
{
    @Autowired
    private AliyunOSSService aliyunOSSService;

    /**
     * 文件图片上传请求 私有写
     */
    @CrossOrigin
    @PostMapping("/uploadOSS")
    public AjaxResult uploadFileOSS(MultipartFile file) {
        try {
            String url = aliyunOSSService.uploadFile(file,"temp");
            String privateReadSignatureUrl = aliyunOSSService.getPrivateReadSignatureUrl(url);
            Map<String,Object> map = new HashMap<>();
            map.put("url",privateReadSignatureUrl);
            map.put("name",file.getOriginalFilename());
            map.put("size",file.getSize());
            return AjaxResult.success(map);
        } catch (Exception e) {
            return AjaxResult.error(e.getMessage());
        }
    }

}

3. 创建OssUrlCleanerUtil图片处理工具类 保存数据库是不需要签名后的url的 所以在新增跟修改的时候需要去签 查询的时候又需要加签,因为多个图片我是以逗号隔开的所以处理了这种情况

/**
 * @Author: suhai
 * @Date: 2025/6/5
 * @Description: 对oss进行加签 和 去签 的工具类
 */
@Component
public class OssUrlCleanerUtil {


    @Autowired
    private AliyunOSSService aliyunOSSService;

    /**
     * 清除URL中的签名参数,只保留基础URL和文件路径。
     * @param urlsStr 带有签名参数的完整URL。
     * @return 清除签名参数后的URL。
     */
    public String cleanUrlsToString(String urlsStr) {
        if (urlsStr == null || urlsStr.trim().isEmpty()) {
            return "";
        }

        // 判断是否包含逗号,决定是多个还是单个URL
        if (urlsStr.contains(",")) {
            String[] urls = urlsStr.split(",");
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < urls.length; i++) {
                String url = urls[i].trim();
                String cleaned = removeQueryString(url);
                sb.append(cleaned);
                if (i < urls.length - 1) {
                    sb.append(","); // 拼接逗号
                }
            }
            return sb.toString();
        } else {
            return removeQueryString(urlsStr.trim());
        }
    }

    /**
     * 辅助方法:清除单个URL中的查询字符串(签名部分)
     */
    private String removeQueryString(String url) {
        int queryIndex = url.indexOf("?");
        if (queryIndex != -1) {
            return url.substring(0, queryIndex);
        }
        return url;
    }


    //加签访问
    public String getSignatureUrl(String urlOrUrls) {
        if (urlOrUrls == null || urlOrUrls.trim().isEmpty()) {
            return "";
        }

        // 判断是否为多个URL
        if (urlOrUrls.contains(",")) {
            String[] urls = urlOrUrls.split(",");
            StringBuilder signedUrls = new StringBuilder();
            for (int i = 0; i < urls.length; i++) {
                String originalUrl = urls[i].trim();
                String signedUrl;
                try {
                    signedUrl = aliyunOSSService.getPrivateReadSignatureUrl(originalUrl);
                } catch (MalformedURLException e) {
                    throw new RuntimeException("生成签名URL失败: " + originalUrl, e);
                }
                if (signedUrl != null && !signedUrl.isEmpty()) {
                    signedUrls.append(signedUrl);
                    if (i < urls.length - 1) {
                        signedUrls.append(",");
                    }
                }
            }
            return signedUrls.toString();
        } else {
            // 单个URL处理
            try {
                return aliyunOSSService.getPrivateReadSignatureUrl(urlOrUrls.trim());
            } catch (MalformedURLException e) {
                throw new RuntimeException("生成签名URL失败: " + urlOrUrls, e);
            }
        }
    }
}

🛡️ 六、安全建议与最佳实践

项目建议
AccessKey使用 RAM 子账号的 AK,避免使用主账号
权限控制按最小权限原则分配策略
签名URL设置合理过期时间,避免长期暴露
日志审计启用 OSS 访问日志和 RAM 操作日志
敏感信息不要硬编码在代码中,使用配置中心或环境变量

🧩 七、扩展功能建议

  • 文件分片上传(大文件)
  • 图片缩略图处理(ImageStyle)
  • 多线程上传优化
  • 结合 Spring Boot 构建 REST API 接口
  • 上传前校验 MIME 类型、大小限制等

📚 总结

本文介绍了 Java 如何通过官方 SDK 对接阿里云 OSS,实现了私有 Bucket 的文件上传、下载、删除以及签名 URL 的生成。通过合理的权限控制和签名机制,我们可以在保证安全的前提下灵活地进行文件操作。

选择 private 权限是为了保护数据安全,而签名 URL 则是实现可控访问的重要工具。两者结合,既能保障数据隐私,又能满足业务需求。


📌 源码地址:[GitHub 示例工程(请自行补充)]

📘 参考文档


💬 如果你有任何问题或想了解更深入的内容,欢迎留言交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值