| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |