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