在现代应用开发中,对象存储服务(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 示例工程(请自行补充)]
📘 参考文档:
💬 如果你有任何问题或想了解更深入的内容,欢迎留言交流!