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