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