From 39f606bc3007e442c04bf9a502617b42b02c02cb Mon Sep 17 00:00:00 2001
From: zhaowenyu <35589726@qq.com>
Date: Thu, 05 Feb 2026 17:16:41 +0800
Subject: [PATCH] 2026-02-04 优化minio中转接口

---
 /dev/null                                                                                  |  129 ----------------
 ard-sample/pom.xml                                                                         |    7 
 ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioFixedCredentialProxy.java |  292 ++++++++++++++++++++++++++++++++++++
 3 files changed, 299 insertions(+), 129 deletions(-)

diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioFixedCredentialProxy.java b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioFixedCredentialProxy.java
new file mode 100644
index 0000000..b60e3d7
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioFixedCredentialProxy.java
@@ -0,0 +1,292 @@
+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 software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.signer.Aws4Signer;
+import software.amazon.awssdk.auth.signer.params.Aws4SignerParams;
+import software.amazon.awssdk.http.SdkHttpFullRequest;
+import software.amazon.awssdk.http.SdkHttpMethod;
+import software.amazon.awssdk.regions.Region;
+
+import javax.net.ssl.*;
+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.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+@Api(tags = "机场上传minio")
+@Slf4j
+@RestController
+@RequestMapping("/minioTransfer")
+public class MinioFixedCredentialProxy {
+
+    // 固定写死 MinIO 管理员凭证
+    private static final String FIXED_AK = "admin";
+    private static final String FIXED_SK = "ard12345";
+    private static final String REGION = "us-east-1";
+    private static final String SERVICE_NAME = "s3";
+
+    @Value("${oss.endpoint}")
+    private String minioEndpoint;
+
+    // 固定配置参数
+    private static final String PROXY_PREFIX = "/minioTransfer";
+    private static final int BUFFER_SIZE = 8192;
+    private static final int CONNECT_TIMEOUT = 10000;
+    private static final int READ_TIMEOUT = 300000;
+
+    // 静态初始化:禁用SSL证书校验,适配私有自签名HTTPS MinIO
+    static {
+        disableSslVerification();
+    }
+
+    /**
+     * 禁用HTTPS证书验证,解决自签名证书握手失败问题
+     */
+    private static void disableSslVerification() {
+        try {
+            TrustManager[] trustAllCerts = new TrustManager[]{
+                    new X509TrustManager() {
+                        @Override
+                        public X509Certificate[] getAcceptedIssuers() {
+                            return new X509Certificate[0];
+                        }
+                        @Override
+                        public void checkClientTrusted(X509Certificate[] certs, String authType) {}
+                        @Override
+                        public void checkServerTrusted(X509Certificate[] certs, String authType) {}
+                    }
+            };
+            SSLContext sc = SSLContext.getInstance("TLS");
+            sc.init(null, trustAllCerts, new SecureRandom());
+            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+
+            // 信任所有主机名,避免域名校验失败
+            HostnameVerifier allHostsValid = (hostname, session) -> true;
+            HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
+        } catch (Exception e) {
+            log.warn("SSL证书校验禁用失败,可能影响HTTPS MinIO连接", e);
+        }
+    }
+
+    /**
+     * 生成S3 V4规范要求的ISO8601格式时间戳 (UTC时区)
+     */
+    private String generateAmzDate() {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
+        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+        return sdf.format(new Date());
+    }
+
+    @ApiOperation("MinIO固定凭证代理(忽略客户端所有签名/凭证)")
+    @RequestMapping("/**")
+    public void proxyRequest(HttpServletRequest request, HttpServletResponse response) {
+        String method = request.getMethod();
+        String requestUri = request.getRequestURI();
+        String queryString = request.getQueryString();
+
+        log.info("代理请求信息 | 方法:{} | 路径:{} | 参数:{}", method, requestUri, queryString);
+
+        // 拼接MinIO目标地址,移除代理前缀
+        String targetPath = requestUri.substring(PROXY_PREFIX.length());
+        StringBuilder targetUrlBuilder = new StringBuilder(minioEndpoint).append(targetPath);
+        if (queryString != null && !queryString.isBlank()) {
+            targetUrlBuilder.append("?").append(queryString);
+        }
+
+        HttpURLConnection conn = null;
+        InputStream responseStream = null;
+        try {
+            URL targetUrl = new URL(targetUrlBuilder.toString());
+            conn = (HttpURLConnection) targetUrl.openConnection();
+
+            // 基础连接配置
+            conn.setRequestMethod(method);
+            conn.setConnectTimeout(CONNECT_TIMEOUT);
+            conn.setReadTimeout(READ_TIMEOUT);
+            conn.setDoInput(true);
+            // 非GET/HEAD请求开启输出流
+            boolean needOutput = !("GET".equalsIgnoreCase(method) || "HEAD".equalsIgnoreCase(method));
+            conn.setDoOutput(needOutput);
+            conn.setInstanceFollowRedirects(false);
+            conn.setUseCaches(false);
+
+            // 收集请求头,过滤所有S3鉴权相关头部
+            Map<String, String> requestHeaders = new HashMap<>();
+            Enumeration<String> headerNames = request.getHeaderNames();
+            if (headerNames != null) {
+                while (headerNames.hasMoreElements()) {
+                    String name = headerNames.nextElement();
+                    // 跳过客户端的签名、token等S3鉴权头
+                    if (isS3AuthHeader(name)) {
+                        continue;
+                    }
+                    String value = request.getHeader(name);
+                    requestHeaders.put(name, value);
+                }
+            }
+
+            // 生成V4签名(自动补充时间头,覆盖所有鉴权逻辑)
+            String authHeader = generateV4Signature(method, targetUrl, queryString, requestHeaders);
+
+            // 将签名后的所有请求头(含时间头)写入连接
+            for (Map.Entry<String, String> entry : requestHeaders.entrySet()) {
+                conn.setRequestProperty(entry.getKey(), entry.getValue());
+            }
+            // 写入最终签名头
+            conn.setRequestProperty("Authorization", authHeader);
+
+            // 流式转发请求体,支持大文件,不占用内存
+            if (needOutput) {
+                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);
+                    }
+                }
+            }
+
+            // 获取响应状态并回传给客户端
+            int statusCode = conn.getResponseCode();
+            response.setStatus(statusCode);
+
+            // 透传所有响应头,支持多值头部
+            Map<String, List<String>> headerFields = conn.getHeaderFields();
+            for (Map.Entry<String, List<String>> entry : headerFields.entrySet()) {
+                String key = entry.getKey();
+                List<String> values = entry.getValue();
+                if (key != null && values != null && !values.isEmpty()) {
+                    for (String val : values) {
+                        response.addHeader(key, val);
+                    }
+                }
+            }
+
+            // 区分正常响应流和错误流
+            try {
+                responseStream = conn.getInputStream();
+            } catch (IOException e) {
+                responseStream = conn.getErrorStream();
+            }
+
+            // 流式回传响应数据
+            if (responseStream != null) {
+                try (OutputStream out = response.getOutputStream()) {
+                    byte[] buffer = new byte[BUFFER_SIZE];
+                    int len;
+                    while ((len = responseStream.read(buffer)) != -1) {
+                        out.write(buffer, 0, len);
+                    }
+                }
+            }
+
+            log.info("代理转发成功 | 目标地址:{} | 响应码:{}", targetUrlBuilder, statusCode);
+
+        } catch (Exception e) {
+            log.error("代理请求执行异常,目标地址:{}", targetUrlBuilder, e);
+            try {
+                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "文件代理上传服务异常");
+            } catch (IOException ex) {
+                log.error("响应错误信息失败", ex);
+            }
+        } finally {
+            // 安全关闭资源,防止句柄泄漏
+            if (responseStream != null) {
+                try {
+                    responseStream.close();
+                } catch (IOException e) {
+                    log.warn("关闭响应流失败", e);
+                }
+            }
+            if (conn != null) {
+                conn.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 过滤S3协议鉴权相关请求头
+     */
+    private boolean isS3AuthHeader(String headerName) {
+        String lowerName = headerName.toLowerCase(Locale.ROOT);
+        return lowerName.startsWith("authorization")
+                || lowerName.startsWith("x-amz-")
+                || lowerName.startsWith("amz-");
+    }
+
+    /**
+     * 生成AWS S3 V4标准签名(适配SDK 2.25.0,手动添加时间头)
+     */
+    private String generateV4Signature(String httpMethod, URL targetUrl, String queryStr, Map<String, String> headers) {
+        // 类型转换:适配AWS SDK要求的 Map<String, List<String>> 格式
+        Map<String, List<String>> sdkHeaders = new HashMap<>();
+        for (Map.Entry<String, String> entry : headers.entrySet()) {
+            sdkHeaders.put(entry.getKey(), Collections.singletonList(entry.getValue()));
+        }
+
+        // 手动添加S3强制要求的时间头,修复缺失时间头报错
+        String amzDate = generateAmzDate();
+        sdkHeaders.put("X-Amz-Date", Collections.singletonList(amzDate));
+
+        try {
+            // 构建签名请求对象
+            SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder()
+                    .method(SdkHttpMethod.valueOf(httpMethod))
+                    .uri(targetUrl.toURI())
+                    .headers(sdkHeaders);
+
+            // 解析查询参数
+            if (queryStr != null && !queryStr.isBlank()) {
+                Arrays.stream(queryStr.split("&"))
+                        .map(param -> param.split("=", 2))
+                        .forEach(parts -> {
+                            if (parts.length == 2) {
+                                requestBuilder.appendRawQueryParameter(parts[0], parts[1]);
+                            } else if (parts.length == 1) {
+                                requestBuilder.appendRawQueryParameter(parts[0], "");
+                            }
+                        });
+            }
+
+            // 配置签名参数(兼容SDK 2.25.0,无高版本方法)
+            AwsBasicCredentials credentials = AwsBasicCredentials.create(FIXED_AK, FIXED_SK);
+            Aws4SignerParams signerParams = Aws4SignerParams.builder()
+                    .awsCredentials(credentials)
+                    .signingRegion(Region.of(REGION))
+                    .signingName(SERVICE_NAME)
+                    .build();
+
+            // 执行签名
+            Aws4Signer signer = Aws4Signer.create();
+            SdkHttpFullRequest signedRequest = signer.sign(requestBuilder.build(), signerParams);
+
+            // 将签名后的完整头部同步回原集合
+            headers.clear();
+            signedRequest.headers().forEach((key, values) -> {
+                if (values != null && !values.isEmpty()) {
+                    headers.put(key, values.get(0));
+                }
+            });
+
+            // 返回签名认证头
+            return signedRequest.headers().get("Authorization").get(0);
+
+        } catch (URISyntaxException e) {
+            log.error("URL转换为URI格式异常: {}", targetUrl, e);
+            throw new RuntimeException("请求地址格式非法", e);
+        }
+    }
+}
\ 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
deleted file mode 100644
index c17737e..0000000
--- a/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioStreamProxyController.java
+++ /dev/null
@@ -1,129 +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.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
diff --git a/ard-sample/pom.xml b/ard-sample/pom.xml
index eae7ce1..0397d7b 100644
--- a/ard-sample/pom.xml
+++ b/ard-sample/pom.xml
@@ -38,6 +38,13 @@
             <artifactId>jackson-dataformat-xml</artifactId>
         </dependency>
 
+        <!-- AWS S3 签名核心依赖,用于生成V4签名 -->
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>auth</artifactId>
+            <version>2.25.0</version>
+        </dependency>
+
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>

--
Gitblit v1.9.3