zhaowenyu
2026-02-05 39f606bc3007e442c04bf9a502617b42b02c02cb
2026-02-04 优化minio中转接口
1 files added
1 files modified
1 files deleted
428 ■■■■■ changed files
ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioFixedCredentialProxy.java 292 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioStreamProxyController.java 129 ●●●●● patch | view | raw | blame | history
ard-sample/pom.xml 7 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioFixedCredentialProxy.java
New file
@@ -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);
        }
    }
}
ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioStreamProxyController.java
File was deleted
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>