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 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(); } } }