SpringBoot 配置 华为云OBS

2532 字
13 分钟
SpringBoot 配置 华为云OBS

SpringBoot 配置 华为云OBS#

1. 导入依赖包#

参考文档:下载与安装SDK(Java SDK)_Java_SDK参考_对象存储服务 OBS-华为云

pom文件中加入以下依赖项,其中3.25.7可以通过Maven Repository: com.huaweicloud » esdk-obs-java-bundle获取

<dependency>
<groupId>com.huaweicloud</groupId>
<artifactId>esdk-obs-java-bundle</artifactId>
<version>3.25.7</version>
</dependency>

2. 配置application.yml#

application.yml中加入,待后续配置使用

huawei:
obs:
endpoint: # 终结点
accessKey: # 访问AK
secretAccessKey: # 访问SK
bucketName: # 访问桶
expiration: # 过期时间(未使用

3.配置ObsProperties.java#

package cn.civer.blog.Config.Properties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ConfigurationProperties(prefix = "huawei.obs")
public class ObsProperties {
private String endpoint;
private String accessKey;
private String secretAccessKey;
private String bucketName;
private Long expiration;
}

4. 配置ObsConfig.java#

package cn.civer.blog.Config;
import cn.civer.blog.Config.Properties.ObsProperties;
import com.obs.services.ObsClient;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
// 显式启用配置属性
@EnableConfigurationProperties(ObsProperties.class)
// 自动生成构造函数并自动装配
@RequiredArgsConstructor
public class ObsConfig {
private final ObsProperties obsProperties;
// 注册并获取ObsClient实例
@Bean
public ObsClient obsClient() {
return new ObsClient(obsProperties.getAccessKey(),
obsProperties.getSecretAccessKey(),
obsProperties.getEndpoint());
}
}

5. 编写ObsUtils.java#

package cn.civer.blog.Utils;
import cn.civer.blog.Config.Properties.ObsProperties;
import com.obs.services.ObsClient;
import com.obs.services.model.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
@Slf4j
@Component
@ConditionalOnBean(ObsClient.class) // 确保ObsClient存在时才注册
@RequiredArgsConstructor
public class ObsUtils {
private final ObsProperties obsProperties;
private final ObsClient obsClient;
/**
* 上传本地文件到OBS (基于File的上传)
* @param filePath 本地文件完整路径
* @return 文件在OBS的访问URL
*/
public String uploadFile(String filePath) {
return uploadFile(filePath, null);
}
/**
* 上传本地文件到OBS,并可指定目标路径
* @param filePath 本地文件完整路径
* @param objectKey 存储在OBS中的路径(如"images/pic.jpg"),为null则使用随机生成的文件名
* @return 文件在OBS的访问URL
*/
public String uploadFile(String filePath, String objectKey) {
filePath = cleanPath(filePath);
objectKey = cleanPath(objectKey);
try {
File file = new File(filePath);
if (!StringUtils.hasText(objectKey)) {
objectKey = generateObjectKey(file.getName());
}
PutObjectResult result = obsClient.putObject(obsProperties.getBucketName(), objectKey, file);
log.info("文件上传成功,ETag: {}", result.getEtag());
return generateFileUrl(objectKey);
} catch (Exception e) {
log.error("上传文件到OBS失败: {}", e.getMessage(), e);
throw new RuntimeException("文件上传失败", e);
}
}
/**
* 流式上传 - 适用于MultipartFile(前端上传)
* @param file 前端上传的文件对象
* @return 文件在OBS的访问URL
*/
public String uploadStream(MultipartFile file) {
return uploadStream(file, null);
}
/**
* 流式上传 - 适用于MultipartFile,并可指定目标路径
* @param file 前端上传的文件对象
* @param objectKey 存储在OBS中的路径,为null则使用原始文件名
* @return 文件在OBS的访问URL
*/
public String uploadStream(MultipartFile file, String objectKey) {
objectKey = cleanPath(objectKey);
try (InputStream inputStream = file.getInputStream()) {
if (!StringUtils.hasText(objectKey)) {
objectKey = generateObjectKey(file.getOriginalFilename());
}
// 创建上传请求并设置元数据
PutObjectRequest request = new PutObjectRequest();
request.setBucketName(obsProperties.getBucketName());
request.setObjectKey(objectKey);
request.setInput(inputStream);
// 设置对象元数据,如Content-Type
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(file.getContentType());
metadata.setContentLength(file.getSize());
request.setMetadata(metadata);
PutObjectResult result = obsClient.putObject(request);
log.info("流上传成功,ETag: {}", result.getEtag());
return generateFileUrl(objectKey);
} catch (IOException e) {
log.error("读取文件流失败: {}", e.getMessage(), e);
throw new RuntimeException("文件处理失败", e);
} catch (Exception e) {
log.error("流式上传到OBS失败: {}", e.getMessage(), e);
throw new RuntimeException("文件上传失败", e);
}
}
/**
* 流式上传 - 适用于通用InputStream
* @param inputStream 输入流
* @param objectKey 存储在OBS中的路径(必须指定)
* @param contentType 文件MIME类型
* @return 文件在OBS的访问URL
*/
public String uploadStream(InputStream inputStream, String objectKey, String contentType) {
try {
objectKey = cleanPath(objectKey);
PutObjectRequest request = new PutObjectRequest();
request.setBucketName(obsProperties.getBucketName());
request.setObjectKey(objectKey);
request.setInput(inputStream);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(contentType);
request.setMetadata(metadata);
PutObjectResult result = obsClient.putObject(request);
log.info("流上传成功,ETag: {}", result.getEtag());
return generateFileUrl(objectKey);
} catch (Exception e) {
log.error("流式上传到OBS失败: {}", e.getMessage(), e);
throw new RuntimeException("文件上传失败", e);
}
}
/**
* 从OBS下载文件
* @param objectKey 文件在OBS的路径
* @return 包含文件流和信息的ObsObject
*/
public ObsObject downloadFile(String objectKey) {
try {
objectKey = cleanPath(objectKey);
ObsObject obsObject = obsClient.getObject(obsProperties.getBucketName(), objectKey);
log.info("文件下载成功: {}", objectKey);
return obsObject;
} catch (Exception e) {
log.error("从OBS下载文件失败: {}", e.getMessage(), e);
throw new RuntimeException("文件下载失败", e);
}
}
/**
* 删除OBS上的文件
* @param objectKey 文件在OBS的路径
* @return 删除是否成功
*/
public boolean deleteFile(String objectKey) {
try {
objectKey = cleanPath(objectKey);
DeleteObjectResult result = obsClient.deleteObject(obsProperties.getBucketName(), objectKey);
log.info("文件删除成功: {}", objectKey);
return true;
} catch (Exception e) {
log.error("从OBS删除文件失败: {}", e.getMessage(), e);
throw new RuntimeException("文件删除失败", e);
}
}
/**
* 列举所有文件
* @return 桶内对象列举结果
*/
public ObjectListing listFile(){
// 简单列举
ObjectListing result = obsClient.listObjects(obsProperties.getBucketName());
return result;
}
/**
* 判断对象是否存在
* @param objectKey 对象所在路径
* @return True Or False
*/
public Boolean doesFileExist(String objectKey){
objectKey = cleanPath(objectKey);
return obsClient.doesObjectExist(obsProperties.getBucketName(), objectKey);
}
/**
* 生成文件的访问URL
* @param objectKey 文件在OBS的路径
* @return 完整的URL
*/
public String generateFileUrl(String objectKey) {
objectKey = cleanPath(objectKey);
// URL格式: https://桶名.域名/对象名
// 根据你的端点配置调整,如果端点是包含域名的完整形式
String endpoint = obsProperties.getEndpoint().replace("https://", "");
//c return "https://" + obsProperties.getBucketName() + "." + endpoint + "/" + objectKey;
return "https://" + obsProperties.getAccessUrl() + "/" + objectKey;
}
/**
* 生成唯一的对象键(文件路径)
* @param originalFileName 原始文件名
* @return 格式如 "2025/10/05/uuid.jpg"
*/
public String generateObjectKey(String originalFileName) {
originalFileName = cleanPath(originalFileName);
String extension = "";
if (StringUtils.hasText(originalFileName) && originalFileName.contains(".")) {
extension = originalFileName.substring(originalFileName.lastIndexOf("."));
} else if (originalFileName == null) {
extension = ".dat";
}
// 使用UUID
String uuid = UUID.randomUUID().toString().replace("-", "");
// 使用原始名
// 按日期分类存储,便于管理
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy/MM/dd");
String datePath = java.time.LocalDate.now().format(formatter);
// 使用UUID
// return datePath + "/" + uuid + extension;
// 使用原始名
return datePath + "/" + originalFileName;
}
/**
* 清理路径前后的空格
* @param path 路径
* @return 清理后的路径String
*/
public String cleanPath(String path){
// 去除前后的空格
path = path.trim();
// 去除可能存在的前导斜杠
if (path.startsWith("/")) {
path = path.substring(1);
}
return path;
}
}

6. Controller类参考#

package cn.civer.blog.Controller.Common;
import cn.civer.blog.Model.Entity.MessageConstants;
import cn.civer.blog.Model.Entity.Result;
import cn.civer.blog.Service.FileServ;
import com.obs.services.model.ObjectListing;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
@RestController
@RequestMapping("/api/file")
public class ObsFileController {
@Autowired
private FileServ fileServ;
/**
* 上传文件
* @param file 设置文件类型为 MediaType.MULTIPART_FORM_DATA_VALUE
* @return 上传成功后的文件访问url
*/
@PreAuthorize("hasRole('subscriber')")
@Operation(summary = "上传文件")
@PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result upLoad(@RequestParam("file") @RequestPart("file") MultipartFile file){
String url = fileServ.uploadFile(file);
return Result.success(url);
}
/**
* 流式下载文件
* @param objectKey 文件所在路径
* @return StreamingResponseBody异步下载流
*/
@Operation(summary = "下载文件")
@GetMapping(value = "download/stream")
public ResponseEntity<StreamingResponseBody> download(@RequestParam("objectKey") String objectKey) {
return fileServ.downloadFile(objectKey);
}
/**
* 删除文件
* @param filePath 文件所在路径
* @return 删除结果
*/
@PreAuthorize("hasRole('subscriber')")
@Operation(summary = "删除文件")
@DeleteMapping(value = "delete")
public Result delete(@RequestParam("filePath") String filePath){
Boolean result = fileServ.deleteFile(filePath);
if(!result){
return Result.success(MessageConstants.FILE_DELETE_FAILED);
}
return Result.success(MessageConstants.FILE_DELETE_SUCCESS);
}
/**
* 获取所有文件列表
* @return 文件列表
*/
@Operation(summary = "获取所有文件")
@GetMapping(value = "select")
public Result select(){
ObjectListing result = fileServ.listFile();
return Result.success(result);
}
}

7. Service类参考#

serv接口#

package cn.civer.blog.Service;
import com.obs.services.model.ObjectListing;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
public interface FileServ {
String uploadFile(MultipartFile file);
Boolean deleteFile(String objectKey);
ObjectListing listFile();
ResponseEntity<StreamingResponseBody> downloadFile(String objectKey);
}

impl实现#

package cn.civer.blog.Service.Impl;
import cn.civer.blog.Exception.BizException;
import cn.civer.blog.Model.Entity.MessageConstants;
import cn.civer.blog.Service.FileServ;
import cn.civer.blog.Utils.ObsUtils;
import com.obs.services.model.ObjectListing;
import com.obs.services.model.ObsObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@Slf4j
@Service
public class FileServImpl implements FileServ {
@Autowired
private ObsUtils obsUtils;
/**
* 根据前端所传文件进行上传
* @param file 待上传的文件
* @return 上传后的url
*/
public String uploadFile(MultipartFile file) {
// 1. 校验
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException(MessageConstants.FILE_NOT_EXIST);
}
// 2. 检查是否已存在同名文件
String objectKey = obsUtils.generateObjectKey(file.getOriginalFilename());
if (obsUtils.doesFileExist(objectKey)) {
log.warn(MessageConstants.FILE_EXIST +": {}", objectKey);
// 可以选择覆盖、重命名、或返回已存在文件URL
return obsUtils.generateFileUrl(objectKey);
}
// 3. 上传
String url = obsUtils.uploadStream(file, objectKey);
// 4. TODO 可在数据库中记录文件信息(如文件名、URL、上传者)
// fileRepository.save(...)
// 5. 返回结果
return url;
}
/**
* 删除文件的业务逻辑封装
* @param objectKey OBS路径
* @return 删除结果
*/
public Boolean deleteFile(String objectKey) {
if (!obsUtils.doesFileExist(objectKey)) {
throw new BizException(MessageConstants.FILE_NOT_EXIST);
}
return obsUtils.deleteFile(objectKey);
}
/**
* 列举所有对象
* @return 列举结果
*/
public ObjectListing listFile(){
ObjectListing result = obsUtils.listFile();
for(ObsObject obsObject : result.getObjects()){
obsObject.getObjectKey();
obsObject.getObjectContent();
obsObject.getOwner();
}
return result;
}
/**
* 流式下载文件
* @param objectKey 文件所在路径
* @return StreamingResponseBody异步下载流
*/
@Override
public ResponseEntity<StreamingResponseBody> downloadFile(String objectKey) {
// 获取ObsObject
ObsObject obsObject = obsUtils.downloadFile(objectKey);
// 获取文件输入流
InputStream inputStream = obsObject.getObjectContent();
// 文件名
String fileName = obsObject.getObjectKey();
// 使用UTF8编码,解决中文乱码问题
String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
// 设置请求头
HttpHeaders headers = new HttpHeaders();
// 设置文件类型
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 设置文件访问类型,inline为预览模式,attachment为直接下载
headers.setContentDispositionFormData("inline", encodeFileName);
// 设置异步下载流并关闭
StreamingResponseBody stream = outputStream -> {
inputStream.transferTo(outputStream);
inputStream.close();
};
return new ResponseEntity<>(stream, headers, HttpStatus.OK);
}
}

补充一点:华为云OBS权限配置建议#

  1. 华为云新建两个用户,分别是OBS-R(用于EdgeOne配置加速)和OBS-RW(用于操作桶以及对象)
  2. 分别获取两个用户的AK和SK,牢记
  3. 华为云OBS对应桶配置“桶策略”,配置两条,一条为OBS-R只读,一天为OBS-RW读写
  4. 购买EdgeOne免费版套餐,配置对象存储加速,选中私有Key预检,填入上面OBS-R用户的AK和SK,后续可通过该域名直接访问对象
  5. OBS-RW提供桶对象的上传、删除等操作

问题#

  1. 配置了Spring Security,又由于ResponseEntity<StreamResponseBody>会触发第二次异步请求下载,从而无法通过Security验证?

    参考链接:[Spring Security 和ResponseEntity<StreamingResponseBody> 结合使用,过滤器执行两次问题分析_responseentity-CSDN博客](https://blog.csdn.net/u014087707/article/details/148873811#:~:text=摘要 当使用ResponseEntity进行大文件下载时,结合Spring,Security的权限验证可能出现过滤器被调用两次的问题。 这是由于StreamingResponseBody会触发异步调度 (DispatcherType.ASYNC),导致安全过滤器链被重复执行。)

    解决办法:在securityConfig中配置一条规则放行异步请求

/**
* Spring Security配置,用于通过权限验证
* @param http
* @param jwtFilter
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtFilter jwtFilter) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
// 登录注册接口放行
.requestMatchers("/api/user/login", "/api/user/register").permitAll()
// 放行异步操作,确保流式文件异步下载正确
.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll()
// Swagger 相关接口(仅在非 prod 环境下放行)
.requestMatchers(getSwaggerWhitelist()).permitAll()
// 其他接口需要认证
.anyRequest().authenticated()
//.anyRequest().permitAll() // 临时放行所有请求
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}

参考文档#

SpringBoot返回文件让前端下载的几种方式_springboot 返回文件给前端-CSDN博客

快速入门(Java SDK)_Java_SDK参考_对象存储服务 OBS-华为云

header中Content-Disposition的作用与使用方法 - wq9 - 博客园

[Spring Security 和ResponseEntity<StreamingResponseBody> 结合使用,过滤器执行两次问题分析_responseentity-CSDN博客](https://blog.csdn.net/u014087707/article/details/148873811#:~:text=摘要 当使用ResponseEntity进行大文件下载时,结合Spring,Security的权限验证可能出现过滤器被调用两次的问题。 这是由于StreamingResponseBody会触发异步调度 (DispatcherType.ASYNC),导致安全过滤器链被重复执行。)

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

SpringBoot 配置 华为云OBS
https://blog.civer.cn/posts/springboot-huaweicloud-obs-config/
作者
Quisper
发布于
2025-10-07
许可协议
CC BY-NC-SA 4.0
最后更新于 2025-10-07,距今已过 154 天

部分内容可能已过时

Profile Image of the Author
Quisper
Hello, I'm Quisper.
公告
溯万物之源起,记一叶之清秋。欢迎来到我的博客!很高兴认识你👋
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
8
分类
2
标签
14
总字数
10,622
运行时长
0
最后活动
0 天前
总访问量
0
总访客数
0

目录