From d6232bede06a3ab278451d3cfab139281b969f19 Mon Sep 17 00:00:00 2001
From: zhaowenyu <35589726@qq.com>
Date: Fri, 06 Feb 2026 17:14:19 +0800
Subject: [PATCH] 2026-02-04 优化minio中转接口

---
 /dev/null                                                                                |  293 -----------------------------
 ard-sample/pom.xml                                                                       |    7 
 ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioStsProxyController.java |  267 ++++++++++++++++++++++++++
 3 files changed, 267 insertions(+), 300 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
deleted file mode 100644
index cc01a8a..0000000
--- a/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioFixedCredentialProxy.java
+++ /dev/null
@@ -1,293 +0,0 @@
-package com.ard.web.controller.sample.media;
-
-import com.ard.common.config.OssConfiguration;
-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 = OssConfiguration.accessKey;  // "admin";
-    private static final String FIXED_SK = OssConfiguration.secretKey;  // "ard12345";
-    private static final String REGION = OssConfiguration.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/MinioStsProxyController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioStsProxyController.java
new file mode 100644
index 0000000..2fc1b51
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioStsProxyController.java
@@ -0,0 +1,267 @@
+package com.ard.web.controller.sample.media;
+
+import com.ard.oss.model.CredentialsDTO;
+import com.ard.oss.service.impl.OssServiceContext;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import okio.BufferedSink;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.net.ssl.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.SecureRandom;
+import java.util.Enumeration;
+import java.util.concurrent.TimeUnit;
+
+@Api(tags = "机场上传minio")
+@Slf4j
+@RestController
+@RequestMapping("/minioTransfer")
+public class MinioStsProxyController {
+
+    // ==================== 常量配置 ====================
+    private static final String PROXY_PREFIX = "/minioTransfer";
+    private static final int BUFFER_SIZE = 32768;
+    private static final String X_AMZ_SECURITY_TOKEN = "X-Amz-Security-Token";
+    private static final String TRANSFER_ENCODING = "Transfer-Encoding";
+    private static final String CONTENT_LENGTH = "Content-Length";
+    private static final String HOST = "Host";
+
+    // 配置文件注入
+    @Value("${oss.endpoint}")
+    private String minioEndpoint;
+
+    private static final Integer connectTimeout=10;
+
+    private static final Integer readWriteTimeout=300;
+
+    @Autowired
+    private OssServiceContext ossService;
+
+    // OkHttp 单例客户端,全局复用
+    private static OkHttpClient httpClient;
+
+    // 静态初始化,仅执行一次
+    static {
+        createUnsafeOkHttpClient();
+    }
+
+    /**
+     * 修复SSL配置:创建忽略证书校验的OkHttp客户端
+     */
+    private static void createUnsafeOkHttpClient() {
+        try {
+            // 1. 创建信任所有证书的TrustManager
+            TrustManager[] trustAllCerts = new TrustManager[]{
+                    new X509TrustManager() {
+                        @Override
+                        public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
+                        @Override
+                        public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
+                        @Override
+                        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
+                            return new java.security.cert.X509Certificate[0];
+                        }
+                    }
+            };
+
+            // 2. 初始化SSL上下文
+            SSLContext sslContext = SSLContext.getInstance("TLS");
+            sslContext.init(null, trustAllCerts, new SecureRandom());
+            SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
+
+            // 3. 构建OkHttp客户端
+            httpClient = new OkHttpClient.Builder()
+                    .sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0])
+                    .hostnameVerifier((hostname, session) -> true)
+                    .connectTimeout(connectTimeout, TimeUnit.SECONDS)
+                    .readTimeout(readWriteTimeout, TimeUnit.SECONDS)
+                    .writeTimeout(readWriteTimeout, TimeUnit.SECONDS)
+                    .connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES))
+                    .retryOnConnectionFailure(true)
+                    .build();
+
+        } catch (Exception e) {
+            log.error("OkHttp SSL配置初始化失败,使用默认客户端", e);
+            httpClient = new OkHttpClient();
+        }
+    }
+
+    @ApiOperation("MinIO STS临时凭证通用代理")
+    @RequestMapping("/**")
+    public void proxyRequest(HttpServletRequest request, HttpServletResponse response) {
+        String method = request.getMethod();
+        String requestUri = request.getRequestURI();
+        String queryString = request.getQueryString();
+        log.info("STS代理请求开始 | 方法:{} | URI:{} | 参数:{}", method, requestUri, queryString);
+
+        // 构造目标地址
+        String targetUrl = buildTargetUrl(requestUri, queryString);
+        if (targetUrl == null) {
+            sendError(response, HttpServletResponse.SC_BAD_REQUEST, "请求路径非法");
+            return;
+        }
+
+        // 获取STS凭证
+        CredentialsDTO credentials = getCredentials();
+        if (credentials == null) {
+            sendError(response, HttpServletResponse.SC_FORBIDDEN, "STS凭证无效");
+            return;
+        }
+
+        try {
+            // 构建请求
+            Request okRequest = buildRequest(request, method, targetUrl, credentials);
+            // 执行并转发响应
+            forwardResponse(okRequest, response);
+            log.info("STS代理请求成功 | 目标地址:{}", targetUrl);
+        } catch (Exception e) {
+            log.error("STS代理请求执行异常", e);
+            sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "代理服务异常");
+        }
+    }
+
+    /**
+     * 构造MinIO请求地址
+     */
+    private String buildTargetUrl(String requestUri, String queryString) {
+        try {
+            String targetPath = requestUri.substring(PROXY_PREFIX.length());
+            StringBuilder urlBuilder = new StringBuilder(minioEndpoint).append(targetPath);
+            if (queryString != null && !queryString.isBlank()) {
+                urlBuilder.append("?").append(queryString);
+            }
+            return urlBuilder.toString();
+        } catch (Exception e) {
+            log.error("目标地址构造失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取并校验STS临时凭证
+     */
+    private CredentialsDTO getCredentials() {
+        try {
+            CredentialsDTO dto = ossService.getCredentials();
+            if (dto == null || dto.getAccessKeyId() == null || dto.getSecurityToken() == null) {
+                log.error("STS凭证核心字段缺失");
+                return null;
+            }
+            log.info("STS凭证获取成功 | AK前缀:{}", dto.getAccessKeyId().substring(0, 4) + "***");
+            return dto;
+        } catch (Exception e) {
+            log.error("获取STS凭证异常", e);
+            return null;
+        }
+    }
+
+    /**
+     * 构建OkHttp请求对象 - 核心修复:移除Content-Length过滤
+     */
+    private Request buildRequest(HttpServletRequest request, String method, String url, CredentialsDTO credentials) throws IOException {
+        Request.Builder builder = new Request.Builder().url(url);
+
+        // 透传原始请求头,仅过滤Host头(OkHttp自动管理),保留Content-Length
+        Enumeration<String> headerNames = request.getHeaderNames();
+        while (headerNames.hasMoreElements()) {
+            String name = headerNames.nextElement();
+            // 仅过滤Host,不再过滤Content-Length,修复MissingContentLength错误
+            if (name.equalsIgnoreCase(HOST)) {
+                continue;
+            }
+            builder.addHeader(name, request.getHeader(name));
+        }
+
+        // 强制添加STS安全令牌(MinIO必填)
+        builder.addHeader(X_AMZ_SECURITY_TOKEN, credentials.getSecurityToken());
+
+        // 构造请求体:非GET/HEAD请求携带流数据
+        RequestBody body = null;
+        if (!"GET".equalsIgnoreCase(method) && !"HEAD".equalsIgnoreCase(method)) {
+            // 获取客户端原始Content-Length
+            long contentLength = request.getContentLengthLong();
+            body = new RequestBody() {
+                @Override
+                public MediaType contentType() {
+                    String contentType = request.getContentType();
+                    return contentType != null ? MediaType.parse(contentType) : MediaType.parse("application/octet-stream");
+                }
+
+                // 重写方法,告知OkHttp固定长度,避免转为chunked编码
+                @Override
+                public long contentLength() {
+                    return contentLength;
+                }
+
+                @Override
+                public void writeTo(BufferedSink sink) {
+                    try (InputStream inputStream = request.getInputStream()) {
+                        byte[] buffer = new byte[BUFFER_SIZE];
+                        int len;
+                        while ((len = inputStream.read(buffer)) != -1) {
+                            sink.write(buffer, 0, len);
+                        }
+                        sink.flush();
+                    } catch (IOException e) {
+                        log.error("请求体流写入失败", e);
+                    }
+                }
+            };
+        }
+
+        builder.method(method, body);
+        return builder.build();
+    }
+
+    /**
+     * 执行请求并将响应透传给前端
+     */
+    private void forwardResponse(Request request, HttpServletResponse response) throws IOException {
+        try (Response okResponse = httpClient.newCall(request).execute()) {
+            // 设置响应状态码
+            response.setStatus(okResponse.code());
+
+            // 透传响应头
+            Headers headers = okResponse.headers();
+            for (String name : headers.names()) {
+                if (TRANSFER_ENCODING.equalsIgnoreCase(name)) {
+                    continue;
+                }
+                response.setHeader(name, headers.get(name));
+            }
+
+            // 透传响应体
+            ResponseBody responseBody = okResponse.body();
+            if (responseBody != null) {
+                try (InputStream in = responseBody.byteStream()) {
+                    byte[] buffer = new byte[BUFFER_SIZE];
+                    int len;
+                    while ((len = in.read(buffer)) != -1) {
+                        response.getOutputStream().write(buffer, 0, len);
+                    }
+                    response.getOutputStream().flush();
+                }
+            }
+        }
+    }
+
+    /**
+     * 统一错误响应工具方法
+     */
+    private void sendError(HttpServletResponse response, int code, String msg) {
+        try {
+            response.sendError(code, msg);
+        } catch (IOException e) {
+            log.error("响应错误信息失败", e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/ard-sample/pom.xml b/ard-sample/pom.xml
index 0397d7b..eae7ce1 100644
--- a/ard-sample/pom.xml
+++ b/ard-sample/pom.xml
@@ -38,13 +38,6 @@
             <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