From e9ed3967943149839c061b0821f7274899ed5e98 Mon Sep 17 00:00:00 2001
From: zhaowenyu <35589726@qq.com>
Date: Wed, 04 Feb 2026 16:44:00 +0800
Subject: [PATCH] 2026-02-04 优化minio上传,兼容minio所有接口的转发
---
ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioGeneralProxyController.java | 118 +++++++++++++++++++
/dev/null | 180 ------------------------------
ard-sample/src/main/java/com/ard/sample/config/RestTemplateConfig.java | 22 +++
3 files changed, 140 insertions(+), 180 deletions(-)
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioGeneralProxyController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioGeneralProxyController.java
new file mode 100644
index 0000000..00715f5
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioGeneralProxyController.java
@@ -0,0 +1,118 @@
+package com.ard.web.controller.sample.media;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.util.StreamUtils;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Enumeration;
+
+@Slf4j
+@Api(tags = "机场上传minio")
+@RestController
+@RequestMapping("/minioTransfer")
+public class MinioGeneralProxyController {
+
+ @Value("${oss.endpoint}")
+ private String minioServerEndpoint;
+
+ private final RestTemplate restTemplate;
+ private static final String BASE_PREFIX = "/minioTransfer";
+
+ public MinioGeneralProxyController(RestTemplate restTemplate) {
+ this.restTemplate = restTemplate;
+ }
+
+ @ApiOperation("MinIO原生接口代理(无感中转)")
+ @RequestMapping(value = "/**")
+ public ResponseEntity<byte[]> proxyAllRequest(HttpServletRequest request) {
+ try {
+ // 1. 解析请求基础信息
+ String methodStr = request.getMethod();
+ String rawUri = URLDecoder.decode(request.getRequestURI(), StandardCharsets.UTF_8);
+ String queryString = request.getQueryString();
+
+ log.info("转发请求 | 方法:{} | URI:{} | 参数:{}", methodStr, rawUri, queryString);
+
+ // 2. 构建MinIO目标地址
+ String targetPath = rawUri.substring(BASE_PREFIX.length());
+ StringBuilder targetUrl = new StringBuilder(minioServerEndpoint);
+ targetUrl.append(targetPath);
+ if (queryString != null && !queryString.isBlank()) {
+ targetUrl.append("?").append(queryString);
+ }
+ URI uri = URI.create(targetUrl.toString());
+
+ // 3. 转换为Spring标准HttpMethod(核心修复点)
+ HttpMethod httpMethod = HttpMethod.valueOf(methodStr);
+
+ // 4. 复制请求头+请求体
+ HttpHeaders headers = copyRequestHeaders(request);
+ byte[] requestBody = extractRequestBody(request);
+ HttpEntity<byte[]> requestEntity = new HttpEntity<>(requestBody, headers);
+
+ // 5. 修复exchange方法调用,匹配Spring官方重载方法
+ ResponseEntity<byte[]> response = restTemplate.exchange(
+ uri,
+ httpMethod,
+ requestEntity,
+ byte[].class
+ );
+
+ log.info("转发成功 | 目标地址:{} | 状态码:{}", targetUrl, response.getStatusCode());
+ // 原样回传所有响应信息
+ return new ResponseEntity<>(
+ response.getBody(),
+ response.getHeaders(),
+ response.getStatusCode()
+ );
+
+ } catch (Exception e) {
+ log.error("代理转发执行异常", e);
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body(("代理服务异常:" + e.getMessage()).getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ /**
+ * 透传所有请求头
+ */
+ private HttpHeaders copyRequestHeaders(HttpServletRequest request) {
+ HttpHeaders headers = new HttpHeaders();
+ Enumeration<String> headerNames = request.getHeaderNames();
+ if (headerNames == null) {
+ return headers;
+ }
+ while (headerNames.hasMoreElements()) {
+ String name = headerNames.nextElement();
+ Enumeration<String> values = request.getHeaders(name);
+ headers.addAll(name, Collections.list(values));
+ }
+ // 移除Host头,避免转发地址冲突
+ headers.remove(HttpHeaders.HOST);
+ return headers;
+ }
+
+ /**
+ * 读取请求体数据
+ */
+ private byte[] extractRequestBody(HttpServletRequest request) {
+ try {
+ return StreamUtils.copyToByteArray(request.getInputStream());
+ } catch (IOException e) {
+ log.warn("读取请求体失败", e);
+ return new byte[0];
+ }
+ }
+}
\ No newline at end of file
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioProxyUploadController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioProxyUploadController.java
deleted file mode 100644
index 97a59b7..0000000
--- a/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioProxyUploadController.java
+++ /dev/null
@@ -1,180 +0,0 @@
-package com.ard.web.controller.sample.media;
-
-import com.ard.common.config.OssConfiguration;
-import com.ard.oss.model.CredentialsDTO;
-import com.ard.oss.service.impl.OssServiceContext;
-import com.ard.sample.media.Response.S3ErrorResponse;
-import com.ard.sample.media.Response.S3PutObjectResponse;
-import io.minio.BucketExistsArgs;
-import io.minio.MinioClient;
-import io.minio.PutObjectArgs;
-import io.minio.credentials.StaticProvider;
-import io.minio.errors.ErrorResponseException;
-import io.minio.errors.MinioException;
-import io.minio.messages.ErrorResponse;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.codec.digest.DigestUtils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-
-import javax.servlet.http.HttpServletRequest;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.time.OffsetDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * 适配 MinIO SDK 8.3.7 + 服务器 RELEASE.2024-08-03
- * 承接大疆 PUT 上传请求,中转上传到 MinIO HTTPS 接口
- */
-@Api(tags = "机场上传minio")
-@Slf4j
-@RestController
-@RequestMapping("/minioTransfer") // 根路径,匹配 Nginx 转发的 /dock/、/kmz/ 路径
-public class MinioProxyUploadController {
-
- // 注入你的凭证服务(保持原有注入方式不变)
- @Autowired
- private OssServiceContext ossService;
-
- // S3标准错误码 -> HTTP状态码 映射表(行业通用标准)
- private static final Map<String, HttpStatus> S3_ERROR_CODE_MAP;
-
- // 初始化常见错误映射
- static {
- S3_ERROR_CODE_MAP = new HashMap<>();
- S3_ERROR_CODE_MAP.put("NoSuchBucket", HttpStatus.NOT_FOUND);
- S3_ERROR_CODE_MAP.put("NoSuchKey", HttpStatus.NOT_FOUND);
- S3_ERROR_CODE_MAP.put("AccessDenied", HttpStatus.FORBIDDEN);
- S3_ERROR_CODE_MAP.put("InvalidCredentials", HttpStatus.FORBIDDEN);
- S3_ERROR_CODE_MAP.put("BucketAlreadyExists", HttpStatus.CONFLICT);
- S3_ERROR_CODE_MAP.put("InvalidRequest", HttpStatus.BAD_REQUEST);
- }
-
- /**
- * 完全复刻 MinIO 原生 PUT 上传接口
- * 通配符匹配所有路径,保留原始URI格式
- */
- @ApiOperation("MinIO原生PUT接口代理(无感中转)")
- @PutMapping(value = "/{bucketName}/**", consumes = MediaType.ALL_VALUE)
- public ResponseEntity<?> proxyMinioPutObject(
- @PathVariable String bucketName,
- // 透传大疆设备原始请求体,不修改、不封装
- @RequestBody(required = false) byte[] rawRequestBody,
- HttpServletRequest request
- ) {
- log.info("接收参数bucketName:{}",bucketName);
- // ===================== 核心修复:安全路径解析,替换替代截取,杜绝索引错误 =====================
- // String contextPath = request.getContextPath();
- String rawUri = URLDecoder.decode(request.getRequestURI(), StandardCharsets.UTF_8);
- // 剔除上下文路径,得到纯业务路径
- String purePath = rawUri.substring("/minioTransfer".length());
- String bucketPrefix = "/" + bucketName;
-
- log.info("中转MinIO 路径解析开始 | 原始URI:{} | 纯业务路径:{} | 桶前缀:{}", rawUri, purePath, bucketPrefix);
-
- // 校验路径是否以桶前缀开头
- if (!purePath.startsWith(bucketPrefix)) {
- return buildNativeS3Error("InvalidRequest", "请求路径格式不匹配", rawUri, HttpStatus.BAD_REQUEST);
- }
-
- // 修复点:replace替换前缀,替代 substring 长度截取,彻底解决索引错位问题
- String objectKey = purePath.replaceFirst("^" + bucketPrefix, "");
- // 兜底处理空路径
- if (objectKey.isEmpty()) {
- objectKey = "/";
- }
-
- log.info("中转MinIO 路径解析成功 | 纯业务路径:{} | 桶名:{} | 最终对象路径:{}",
- purePath, bucketName, objectKey);
-
- // byte[] requestData = rawRequestBody == null ? new byte[0] : rawRequestBody;
-
- try {
- // ===================== 2. 复用原有逻辑:获取临时凭证 =====================
- var credentials = ossService.getCredentials();
- if (credentials == null || credentials.getAccessKeyId() == null || credentials.getSecurityToken() == null) {
- log.error("临时凭证获取失败");
- return buildNativeS3Error("InvalidCredentials", "临时凭证无效", rawUri, HttpStatus.FORBIDDEN);
- }
-
- // ===================== 3. 复用原有逻辑:构建MinIO客户端 =====================
- MinioClient minioClient = MinioClient.builder()
- .endpoint(OssConfiguration.endpoint)
- .credentialsProvider(new StaticProvider(
- credentials.getAccessKeyId(),
- credentials.getAccessKeySecret(),
- credentials.getSecurityToken()
- ))
- .build();
-
- // 校验桶存在性(兼容原生接口行为,桶不存在返回404)
- boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
- if (!bucketExists) {
- return buildNativeS3Error("NoSuchBucket", "指定存储桶不存在", bucketName, HttpStatus.NOT_FOUND);
- }
-
- // ===================== 4. 透传请求:原样转发大疆原始数据到MinIO,无业务篡改 =====================
- // 空请求体=大疆原生创建目录请求,直接透传;带请求体=上传文件,原生处理
- byte[] requestBody = rawRequestBody == null ? new byte[0] : rawRequestBody;
- try (ByteArrayInputStream inputStream = new ByteArrayInputStream(requestBody)) {
- minioClient.putObject(
- PutObjectArgs.builder()
- .bucket(bucketName)
- .object(objectKey)
- // 透传原始流、原始长度,完全复刻原生请求
- .stream(inputStream, requestBody.length, -1)
- // 透传原生Content-Type,无修改
- .contentType(request.getContentType() == null ? "application/octet-stream" : request.getContentType())
- .build()
- );
- }
-
- // ===================== 5. 原生接口成功响应:200 OK + 空响应体(核心对齐!)=====================
- log.info("中转上传成功 | 路径:{}", objectKey);
- return ResponseEntity.ok()
- // 无任何响应体,与MinIO原生行为完全一致
- .body(null);
-
- } catch (ErrorResponseException e) {
- // ===================== 最终修正:无任何无效方法调用 =====================
- ErrorResponse nativeError = e.errorResponse();
- String errorCode = nativeError.code();
- // 映射状态码,无匹配则默认500
- HttpStatus status = S3_ERROR_CODE_MAP.getOrDefault(errorCode, HttpStatus.INTERNAL_SERVER_ERROR);
-
- log.error("MinIO服务错误 | 错误码:{} | 信息:{}", errorCode, nativeError.message());
- // 透传MinIO原生XML错误体,大疆设备完美兼容
- return ResponseEntity.status(status)
- .contentType(MediaType.APPLICATION_XML)
- .body(nativeError);
- } catch (MinioException e) {
- log.error("MinIO客户端异常:{}", e.getMessage());
- return buildNativeS3Error("ClientError", e.getMessage(), rawUri, HttpStatus.INTERNAL_SERVER_ERROR);
- } catch (Exception e) {
- log.error("代理中转通用异常:{}", e.getMessage(), e);
- return buildNativeS3Error("InternalError", "服务器代理异常", rawUri, HttpStatus.INTERNAL_SERVER_ERROR);
- }
- }
-
- /**
- * 兜底构造S3标准XML错误响应,兼容大疆设备解析
- */
- private ResponseEntity<S3ErrorResponse> buildNativeS3Error(String code, String message, String resource, HttpStatus status) {
- S3ErrorResponse error = new S3ErrorResponse(code, message, resource);
- return ResponseEntity.status(status)
- .contentType(MediaType.APPLICATION_XML)
- .body(error);
- }
-}
\ No newline at end of file
diff --git a/ard-sample/src/main/java/com/ard/sample/config/RestTemplateConfig.java b/ard-sample/src/main/java/com/ard/sample/config/RestTemplateConfig.java
new file mode 100644
index 0000000..c56f2a5
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/config/RestTemplateConfig.java
@@ -0,0 +1,22 @@
+package com.ard.sample.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestTemplateConfig {
+
+ @Bean
+ public RestTemplate restTemplate() {
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+ // 连接超时:10s
+ factory.setConnectTimeout(10000);
+ // 读取超时:5min(适配大文件上传/下载)
+ factory.setReadTimeout(300000);
+ // 开启流式请求,避免大文件加载到内存
+ factory.setBufferRequestBody(false);
+ return new RestTemplate(factory);
+ }
+}
--
Gitblit v1.9.3