From 08d5077ac4139455e6573f3a61bbae99ca62573e Mon Sep 17 00:00:00 2001
From: zhaowenyu <35589726@qq.com>
Date: Thu, 05 Feb 2026 14:07:04 +0800
Subject: [PATCH] 2026-02-04 优化minio上传,兼容minio所有接口的转发

---
 /dev/null                                                                                   |  118 -----------------------
 ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioStreamProxyController.java |  129 +++++++++++++++++++++++++
 2 files changed, 129 insertions(+), 118 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
deleted file mode 100644
index 00715f5..0000000
--- a/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioGeneralProxyController.java
+++ /dev/null
@@ -1,118 +0,0 @@
-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/MinioStreamProxyController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioStreamProxyController.java
new file mode 100644
index 0000000..c17737e
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioStreamProxyController.java
@@ -0,0 +1,129 @@
+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.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.*;
+import java.util.Enumeration;
+
+/**
+ * MinIO S3协议 原生流式透传代理
+ * 零修改转发请求头、请求体、响应,保证S3签名校验通过
+ */
+@Api(tags = "机场上传minio")
+@Slf4j
+@RestController
+@RequestMapping("/minioTransfer")
+public class MinioStreamProxyController {
+
+    // 配置MinIO服务地址,例如:http://127.0.0.1:9000
+    @Value("${oss.endpoint}")
+    private String minioEndpoint;
+
+    private static final String PROXY_PREFIX = "/minioTransfer";
+    // 流拷贝缓冲区大小(适配大文件)
+    private static final int BUFFER_SIZE = 8192;
+
+    @ApiOperation("MinIO原生接口代理(无感中转)")
+    @RequestMapping("/**")
+    public void streamProxy(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        // 1. 解析原始请求信息
+        String method = request.getMethod();
+        String requestUri = request.getRequestURI();
+        String queryString = request.getQueryString();
+        log.info("S3代理透传 | 方法:{} | 路径:{} | 参数:{}", method, requestUri, queryString);
+
+        // 2. 拼接MinIO目标地址
+        String targetPath = requestUri.substring(PROXY_PREFIX.length());
+        StringBuilder targetUrl = new StringBuilder(minioEndpoint).append(targetPath);
+        if (queryString != null && !queryString.isBlank()) {
+            targetUrl.append("?").append(queryString);
+        }
+
+        // 3. 创建目标连接
+        URL url = new URL(targetUrl.toString());
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        // 配置连接参数
+        conn.setRequestMethod(method);
+        conn.setDoInput(true);
+        conn.setConnectTimeout(10000);
+        conn.setReadTimeout(300000);
+        // 关键:允许写入请求体(POST/PUT/PATCH)
+        conn.setDoOutput(!method.equalsIgnoreCase("GET") && !method.equalsIgnoreCase("HEAD"));
+        // 禁止自动重定向,保持S3协议一致性
+        conn.setInstanceFollowRedirects(false);
+
+        try {
+            // 4. 透传所有请求头(S3签名头完整保留,不修改、不删除)
+            Enumeration<String> headerNames = request.getHeaderNames();
+            if (headerNames != null) {
+                while (headerNames.hasMoreElements()) {
+                    String name = headerNames.nextElement();
+                    String value = request.getHeader(name);
+                    conn.setRequestProperty(name, value);
+                }
+            }
+
+            // 5. 透传请求体(流式拷贝,不加载到内存,保证签名完整性)
+            if (conn.getDoOutput()) {
+                try (InputStream requestIn = request.getInputStream();
+                     OutputStream connOut = conn.getOutputStream()) {
+                    byte[] buffer = new byte[BUFFER_SIZE];
+                    int len;
+                    while ((len = requestIn.read(buffer)) != -1) {
+                        connOut.write(buffer, 0, len);
+                    }
+                }
+            }
+
+            // 6. 获取MinIO响应,透传状态码、响应头、响应体
+            int statusCode = conn.getResponseCode();
+            response.setStatus(statusCode);
+
+            // 透传响应头
+            conn.getHeaderFields().forEach((key, values) -> {
+                if (key != null && values != null && !values.isEmpty()) {
+                    response.setHeader(key, values.get(0));
+                }
+            });
+
+            // 透传响应流(区分正常流/错误流)
+            InputStream responseIn;
+            try {
+                responseIn = conn.getInputStream();
+            } catch (IOException e) {
+                responseIn = conn.getErrorStream();
+            }
+
+            try (OutputStream responseOut = response.getOutputStream()) {
+                byte[] buffer = new byte[BUFFER_SIZE];
+                int len;
+                while ((len = responseIn.read(buffer)) != -1) {
+                    responseOut.write(buffer, 0, len);
+                }
+            } finally {
+                if (responseIn != null) {
+                    responseIn.close();
+                }
+            }
+
+            log.info("S3代理转发成功 | 目标地址:{} | 状态码:{}", targetUrl, statusCode);
+
+        } catch (Exception e) {
+            log.error("S3代理转发异常", e);
+            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "代理服务异常");
+        } finally {
+            // 关闭连接
+            conn.disconnect();
+        }
+    }
+}
\ No newline at end of file

--
Gitblit v1.9.3