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)// 自动生成构造函数并自动装配@RequiredArgsConstructorpublic 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存在时才注册@RequiredArgsConstructorpublic 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@Servicepublic 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权限配置建议
- 华为云新建两个用户,分别是OBS-R(用于EdgeOne配置加速)和OBS-RW(用于操作桶以及对象)
- 分别获取两个用户的AK和SK,牢记
- 华为云OBS对应桶配置“桶策略”,配置两条,一条为OBS-R只读,一天为OBS-RW读写
- 购买EdgeOne免费版套餐,配置对象存储加速,选中私有Key预检,填入上面OBS-R用户的AK和SK,后续可通过该域名直接访问对象
- OBS-RW提供桶对象的上传、删除等操作
问题
-
配置了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*/@Beanpublic 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),导致安全过滤器链被重复执行。)
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
部分内容可能已过时