完整教程:Spring Boot 集成华为云 OBS 实现文件上传(含表单上传签名)
在现代 Web 应用中,文件上传是一个非常常见的功能。为了提高性能和安全性,我们通常会将文件存储到云存储服务中,而不是直接存储在应用服务器上。华为云 OBS (Object Storage Service) 就是这样一种稳定、安全、高可用的对象存储服务。
本文将详细介绍如何在 Spring Boot 项目中集成华为云 OBS,实现两种常见的上传方式:
- 服务端直接上传:客户端将文件上传到我们的应用服务器,服务器再将文件转发到 OBS。
- 基于表单签名上传:服务器生成一个临时的、具有权限的签名,客户端使用这个签名直接向 OBS 上传文件,无需经过应用服务器,大大减轻了服务器压力。
1. 准备工作
在开始编码之前,你需要完成以下准备工作:
- 拥有一个华为云账号,并创建一个 OBS 桶 (Bucket)。
- 获取 Access Key ID 和 Secret Access Key。这是访问 OBS 服务的凭证,可以在华为云控制台的 “我的凭证” 中创建和查看。
- 确保你的 Spring Boot 项目已经创建。
2. 添加依赖
首先,在你的 pom.xml 文件中添加华为云 OBS Java SDK 的依赖。
com.huaweicloud
esdk-obs-java
3.25.10
注:请根据你的 Spring Boot 版本选择兼容的 OBS SDK 版本。
3. 配置 OBS 客户端
我们使用 Spring Boot 的配置注入功能来管理 OBS 的连接信息,并创建一个单例的 ObsClient Bean。
3.1 添加配置文件
在 application.properties 或 application.yml 中添加你的 OBS 配置:
application.properties 示例:
# OBS 访问密钥
huawei.obs.access-key-id=你的Access-Key-ID
huawei.obs.secret-access-key=你的Secret-Access-Key
# OBS 服务地址 endpoint (例如: https://obs.cn-north-1.myhuaweicloud.com)
huawei.obs.endpoint=你的OBS-Endpoint
# OBS 桶名称
huawei.obs.bucket-name=你的Bucket-Name
3.2 创建配置类 ObsConfig.java
这个类负责读取配置并生成 ObsClient Bean。
import com.huaweicloud.sdk.obs.v3.ObsClient;
import com.huaweicloud.sdk.obs.v3.model.ObsConfiguration;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* OBS 配置类
* 用于创建和管理 ObsClient 实例
*/
@Configuration
@Getter // Lombok 注解,自动生成 getter 方法
public class ObsConfig {
// 从配置文件读取 OBS 访问密钥 ID
@Value("${huawei.obs.access-key-id}")
private String accessKeyId;
// 从配置文件读取 OBS 秘密访问密钥
@Value("${huawei.obs.secret-access-key}")
private String secretAccessKey;
// 从配置文件读取 OBS 服务端点
@Value("${huawei.obs.endpoint}")
private String endpoint;
// 从配置文件读取 OBS 桶名称
@Value("${huawei.obs.bucket-name}")
private String bucketName;
/**
* 初始化 OBS 客户端
* @Bean 注解将 ObsClient 实例交由 Spring 容器管理,成为一个单例 Bean
* @return ObsClient
*/
@Bean
public ObsClient obsClient() {
// 1. 创建 OBS 配置对象
ObsConfiguration config = new ObsConfiguration();
// 设置服务端点
config.setEndPoint(endpoint);
// 2. 构造 ObsClient 实例
// 传入访问密钥 ID, 秘密访问密钥和配置对象
return new ObsClient(accessKeyId, secretAccessKey, config);
}
}
4. 创建数据模型
为了方便向前端传递表单上传所需的信息,我们创建一个 ObsForm 类。
ObsForm.java
import lombok.Getter;
import lombok.Setter;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
/**
* 用于返回给前端的 OBS 表单上传所需信息
*/
@Getter
@Setter
@ApiModel(description = "OBS表单上传信息")
public class ObsForm {
@ApiModelProperty(value = "策略")
private String policy;
@ApiModelProperty(value = "访问密钥ID")
private String accessKeyId;
@ApiModelProperty(value = "签名")
private String signature;
@ApiModelProperty(value = "OBS服务端点")
private String endpoint;
@ApiModelProperty(value = "桶名称")
private String bucketname;
}
注:这里使用了 Swagger (Springfox) 的注解来生成 API 文档,如果你没有使用 Swagger,可以移除 @ApiModel 和 @ApiModelProperty 注解。
5. 实现上传接口 (Controller)
现在,我们来创建核心的 ObsController,实现两种上传方式的接口。
ObsController.java
import com.huaweicloud.sdk.obs.v3.ObsClient;
import com.huaweicloud.sdk.obs.v3.model.PostSignatureRequest;
import com.huaweicloud.sdk.obs.v3.model.PostSignatureResponse;
import com.huaweicloud.sdk.obs.v3.model.PutObjectRequest;
import com.huaweicloud.sdk.obs.v3.model.PutObjectResult;
import com.huaweicloud.sdk.obs.v3.model.RESTCannedACL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* OBS 文件上传控制器
*/
@RestController
@RequestMapping("/api/obs") // 统一的接口前缀
public class ObsController {
// 注入由 Spring 容器管理的 ObsClient 实例
@Autowired
private ObsClient obsClient;
// 注入 OBS 配置信息
@Autowired
private ObsConfig obsConfig;
/**
* 方式一:服务端直接上传文件
* 客户端将文件上传到应用服务器,服务器再转发到 OBS
*
* @param file 前端上传的文件 (MultipartFile)
* @return 上传成功后的文件 URL
*/
@PostMapping("/uploadFile")
public Result uploadFile(@RequestParam("file") MultipartFile file) {
try {
// 检查文件是否为空
if (file.isEmpty()) {
return Result.fail("上传文件不能为空");
}
// 1. 构建 PutObjectRequest 请求对象
PutObjectRequest request = new PutObjectRequest();
// 设置要上传到的桶名
request.setBucketName(obsConfig.getBucketName());
// 设置文件的访问权限为公共读,这样可以直接通过 URL 访问
request.setAcl(RESTCannedACL.REST_CANNED_PUBLIC_READ);
// 2. 处理文件名:为了避免重复,使用 UUID 前缀 + 原文件名
String originalFileName = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString().replace("-", "") + "_" + originalFileName;
// 设置对象键(Object Key),即文件在 OBS 中的名称
request.setObjectKey(fileName);
// 3. 设置上传的文件流
request.setInput(file.getInputStream());
// 4. 执行上传操作
PutObjectResult result = obsClient.putObject(request);
return Result.ok(result.getObjectUrl(), "文件上传成功");
} catch (Exception e) {
e.printStackTrace(); // 在生产环境中,应使用日志记录异常
return Result.fail("文件上传失败: " + e.getMessage());
}
}
/**
* 方式二:获取表单上传所需的签名信息
* 服务器生成临时凭证,客户端使用该凭证直接向 OBS 上传文件
*
* @return 包含 policy, signature 等信息的 ObsForm 对象
*/
@GetMapping("/getForm")
public Result getForm() {
try {
// 1. 创建 PostSignatureRequest 请求对象
PostSignatureRequest request = new PostSignatureRequest();
// 2. 设置表单上传的策略(Policy)
// Policy 是一个 JSON 字符串,用于描述允许的上传行为,如过期时间、上传的桶、ACL等
Map formParams = new HashMap<>();
// 设置对象的 ACL 为公共读
formParams.put("x-obs-acl", "public-read");
// 可以限制上传文件的 Content-Type
// formParams.put("content-type", "image/jpeg");
request.setFormParams(formParams);
// 3. 设置签名的有效期(单位:秒)
request.setExpires(3600); // 这里设置为 1 小时
// 4. 生成 Post 签名
PostSignatureResponse response = obsClient.createPostSignature(request);
// 5. 构建返回给前端的表单信息对象
ObsForm obsForm = new ObsForm();
obsForm.setPolicy(response.getPolicy()); // 编码后的 Policy
obsForm.setSignature(response.getSignature()); // 签名
obsForm.setAccessKeyId(obsConfig.getAccessKeyId()); // Access Key ID
obsForm.setEndpoint(obsConfig.getEndpoint()); // OBS Endpoint
obsForm.setBucketname(obsConfig.getBucketName());// Bucket Name
return Result.ok(obsForm, "获取表单签名成功");
} catch (Exception e) {
e.printStackTrace();
return Result.fail("获取表单签名失败: " + e.getMessage());
}
}
}
// 注意:这里需要你有一个自定义的 Result 类来统一 API 返回格式
// 下面是一个简单的 Result 类示例:
class Result {
private int code;
private String msg;
private T data;
// 私有构造函数
private Result() {}
// 成功响应
public static Result ok(T data, String msg) {
Result result = new Result<>();
result.code = 200;
result.msg = msg;
result.data = data;
return result;
}
public static Result ok(T data) {
return ok(data, "操作成功");
}
// 失败响应
public static Result fail(String msg) {
Result result = new Result<>();
result.code = 500;
result.msg = msg;
return result;
}
// Getters and Setters
public int getCode() { return code; }
public String getMsg() { return msg; }
public T getData() { return data; }
}
6. 两种上传方式对比与选择
| 特性 | 服务端直接上传 (/uploadFile) | 客户端表单签名上传 (/getForm + 前端直传) |
|---|---|---|
| 数据路径 | 客户端 -> 应用服务器 -> OBS | 客户端 -> OBS |
| 服务器压力 | 高。需要接收、存储(临时)、转发文件流,消耗 CPU 和带宽。 | 低。仅负责生成签名,不参与文件传输。 |
| 上传速度 | 较慢。受限于应用服务器的上行带宽。 | 较快。客户端直接与 OBS 高速网络交互。 |
| 安全性 | 较高。文件内容经过应用服务器,可以进行校验、过滤、 virus scan 等。 | 中等。签名机制保证了上传权限,但服务器无法在上传前检查文件内容。 |
| 复杂度 | 低。后端逻辑简单,前端就是普通的文件上传。 | 中等。需要前端配合,用 JavaScript 构造 FormData 并上传到 OBS。 |
| 适用场景 | 文件较小、需要在服务端进行业务处理(如日志、审核)、对安全性要求极高的场景。 | 文件较大(如图片、视频)、追求用户体验、希望减轻服务器负担的场景。 |
建议:对于大多数现代 Web 应用,强烈推荐使用客户端表单签名上传的方式,以获得更好的性能和 scalability。
7. 前端如何实现表单直传(示例)
当前端调用 /api/obs/getForm 获取到 policy, signature 等信息后,就可以构造一个 FormData 并发送 POST 请求到 OBS 的地址。
OBS 表单直传示例
OBS 表单直传示例
<script>
async function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('请选择一个文件');
return;
}
try {
// 1. 从你的后端服务获取上传凭证
console.log('正在获取上传凭证...');
const response = await fetch('/api/obs/getForm');
const result = await response.json();
if (result.code !== 200) {
throw new Error('获取上传凭证失败: ' + result.msg);
}
const { policy, signature, accessKeyId, endpoint, bucketname } = result.data;
console.log('获取上传凭证成功');
// 2. 构造 FormData
const formData = new FormData();
formData.append('key', 'prefix/' + Date.now() + '_' + file.name); // 自定义文件在 OBS 中的路径和名称
formData.append('policy', policy);
formData.append('signature', signature);
formData.append('AccessKeyId', accessKeyId);
formData.append('x-obs-acl', 'public-read'); // 必须与后端设置的一致
formData.append('content-type', file.type); // 可选,但建议设置
formData.append('file', file); // 文件对象
// 3. 构造 OBS 的上传地址: https://bucketname.endpoint
const uploadUrl = `https://${bucketname}.${endpoint.replace('https://', '')}`;
console.log('上传地址:', uploadUrl);
// 4. 发送 POST 请求到 OBS
console.log('开始上传文件...');
const uploadResponse = await fetch(uploadUrl, {
method: 'POST',
body: formData
});
if (uploadResponse.ok) {
// 上传成功,OBS 会返回 204 No Content
console.log('文件上传成功!');
// 构建文件的访问 URL
const fileUrl = `${uploadUrl}/${formData.get('key')}`;
console.log('文件URL:', fileUrl);
alert('文件上传成功! URL: ' + fileUrl);
} else {
const errorData = await uploadResponse.text();
throw new Error('文件上传失败: ' + uploadResponse.status + ' ' + errorData);
}
} catch (error) {
console.error('上传过程出错:', error);
alert('上传失败: ' + error.message);
}
}
</script>
8. 总结与注意事项
安全第一:
- 切勿将
Access Key ID和Secret Access Key硬编码在代码中或暴露给前端。 - 使用 IAM (Identity and Access Management) 角色和策略来最小化
Access Key的权限。 - 对于表单上传,设置合理的
Expires时间,并且在Policy中尽可能地限制上传条件(如bucket,key前缀,content-type等)。
- 切勿将
异常处理:代码中加入了基本的异常捕获,但在生产环境中,你需要更完善的异常处理和日志记录。
桶策略和 CORS:
- 确保你的 OBS 桶策略允许
PutObject操作。 - 如果前端页面和 OBS 桶不在同一个域名下,必须为你的 OBS 桶配置 CORS (跨域资源共享) 规则,允许来自你网站域名的
POST请求。否则前端直传会失败。
- 确保你的 OBS 桶策略允许
文件命名:始终使用唯一的文件名(如 UUID)来避免覆盖。
通过本文的步骤,你已经掌握了在 Spring Boot 项目中集成华为云 OBS 进行文件上传的两种核心方法。根据你的业务场景选择合适的方案,并记得遵循最佳实践来保障你的应用安全和稳定。
浙公网安备 33010602011771号