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 requestHeaders = new HashMap<>(); Enumeration 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 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> headerFields = conn.getHeaderFields(); for (Map.Entry> entry : headerFields.entrySet()) { String key = entry.getKey(); List 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 headers) { // 类型转换:适配AWS SDK要求的 Map> 格式 Map> sdkHeaders = new HashMap<>(); for (Map.Entry 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); } } }