zhaowenyu
2026-02-11 81ee4f2a3fd0e0b57880b378025f4bbfd84745fc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
package com.ard.web.controller.sample.media;
 
import com.ard.oss.model.CredentialsDTO;
import com.ard.oss.service.impl.OssServiceContext;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import okio.BufferedSink;
import org.springframework.beans.factory.annotation.Autowired;
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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.concurrent.TimeUnit;
 
/**
 * MinIO STS代理控制器(终极版)
 * 1. 彻底清除客户端鉴权头 + 替换为代理侧STS凭证
 * 2. 强制HTTP通信(绕过SSL连接重置问题)
 * 3. 适配分块上传全流程
 */
@Api(tags = "机场上传minio")
@Slf4j
@RestController
@RequestMapping("/minioTransfer")
public class MinioStsProxyController {
 
    // ==================== 常量配置 ====================
    private static final String PROXY_PREFIX = "/minioTransfer";
    private static final int BUFFER_SIZE = 65536; // 64K缓冲区
    private static final long MAX_MEMORY_SIZE = 1024 * 1024 * 100; // 100MB
    private static final String X_AMZ_SECURITY_TOKEN = "X-Amz-Security-Token";
    private static final String TRANSFER_ENCODING = "Transfer-Encoding";
    private static final String HOST = "Host";
 
    // 需清除的客户端鉴权头(核心:避免和代理侧凭证冲突)
    private static final String[] AUTH_HEADERS_TO_REMOVE = {
            "Authorization", "X-Amz-Access-Key-Id", "X-Amz-Secret-Access-Key",
            "X-Amz-Security-Token", "AccessKey", "SecretKey", "Token"
    };
 
    // 配置注入
    @Value("${oss.endpoint}")
    private String minioEndpoint;
    private Integer connectTimeout = 30;
    private Integer readWriteTimeout = 1800;
 
    @Autowired
    private OssServiceContext ossService;
 
    // OkHttp客户端(禁用SSL,强制HTTP,最大化兼容性)
    private static OkHttpClient httpClient;
 
    static {
        // 构建纯HTTP客户端(彻底绕过SSL问题)
        httpClient = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(1800, TimeUnit.SECONDS) // 30分钟超时
                .writeTimeout(1800, TimeUnit.SECONDS)
                .connectionPool(new ConnectionPool(30, 10, TimeUnit.MINUTES))
                .retryOnConnectionFailure(true) // 开启重试
                .pingInterval(10, TimeUnit.SECONDS) // TCP保活
                .callTimeout(60, TimeUnit.SECONDS)
                .build();
 
        log.info("OkHttp客户端初始化完成(纯HTTP模式,禁用SSL)");
    }
 
    /**
     * 核心代理方法:清除客户端鉴权 + 替换为代理侧STS凭证
     */
    @ApiOperation("MinIO STS代理(清除客户端鉴权+强制HTTP)")
    @RequestMapping("/**")
    public void proxyRequest(HttpServletRequest request, HttpServletResponse response) {
        String method = request.getMethod();
        String requestUri = request.getRequestURI();
        String queryString = request.getQueryString();
        long contentLength = request.getContentLengthLong();
 
        log.info("STS代理请求开始 | 方法:{} | URI:{} | 参数:{} | 内容长度:{}B",
                method, requestUri, queryString, contentLength);
 
        // 1. 构造目标地址(强制转换为HTTP,无论配置的是HTTPS)
        String targetUrl = buildTargetUrl(requestUri, queryString);
        if (targetUrl == null) {
            sendError(response, HttpServletResponse.SC_BAD_REQUEST, "请求路径非法");
            return;
        }
 
        // 2. 获取代理侧STS凭证(核心:替换客户端鉴权)
        CredentialsDTO credentials = getCredentials();
        if (credentials == null) {
            sendError(response, HttpServletResponse.SC_FORBIDDEN, "STS凭证无效");
            return;
        }
 
        // 3. 构建请求(清除客户端鉴权 + 注入代理侧凭证)
        try {
            Request okRequest = buildRequest(request, method, targetUrl, credentials);
            forwardResponse(okRequest, response);
            log.info("STS代理请求成功 | 目标地址:{}", targetUrl);
        } catch (Exception e) {
            log.error("STS代理请求执行异常", e);
            String errorMsg = e.getMessage().contains("Connection reset") ?
                    "MinIO连接重置(已强制HTTP,需检查MinIO服务是否启动)" : "代理服务异常";
            sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, errorMsg);
        }
    }
 
    /**
     * 构造目标地址:强制转换为HTTP(核心解决SSL问题)
     */
    private String buildTargetUrl(String requestUri, String queryString) {
        try {
            String targetPath = requestUri.substring(PROXY_PREFIX.length());
            // 强制将HTTPS转为HTTP(无论配置的endpoint是啥)
            String httpEndpoint = minioEndpoint.replace("https://", "http://");
            // 修正端口:MinIO HTTPS默认9001 → HTTP默认9000
            if (httpEndpoint.contains(":9001")) {
                httpEndpoint = httpEndpoint.replace(":9001", ":9000");
            }
 
            StringBuilder urlBuilder = new StringBuilder(httpEndpoint).append(targetPath);
            if (queryString != null && !queryString.isBlank()) {
                urlBuilder.append("?").append(queryString);
            }
            String targetUrl = urlBuilder.toString();
            log.info("构造目标地址(强制HTTP):{}", targetUrl);
            return targetUrl;
        } catch (Exception e) {
            log.error("构造目标地址失败", e);
            return null;
        }
    }
 
    /**
     * 获取代理侧STS凭证(确保完整性)
     */
    private CredentialsDTO getCredentials() {
        try {
            CredentialsDTO dto = ossService.getCredentials();
            if (dto == null || dto.getAccessKeyId() == null || dto.getAccessKeySecret() == null || dto.getSecurityToken() == null) {
                log.error("STS凭证核心字段缺失(AK/SK/Token)");
                return null;
            }
            log.info("STS凭证获取成功 | AK前缀:{}", dto.getAccessKeyId().substring(0, 4) + "***");
            return dto;
        } catch (Exception e) {
            log.error("获取STS凭证异常", e);
            return null;
        }
    }
 
    /**
     * 构建请求:彻底清除客户端鉴权头 + 注入代理侧STS凭证
     */
    private Request buildRequest(HttpServletRequest request, String method, String url, CredentialsDTO credentials) throws IOException {
        Request.Builder builder = new Request.Builder().url(url);
 
        // ========== 核心步骤1:清除所有客户端鉴权头 ==========
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            // 跳过Host头(OkHttp自动处理)
            if (name.equalsIgnoreCase(HOST)) {
                continue;
            }
            // 跳过所有鉴权相关头(避免冲突)
            boolean isAuthHeader = false;
            for (String authHeader : AUTH_HEADERS_TO_REMOVE) {
                if (name.equalsIgnoreCase(authHeader)) {
                    isAuthHeader = true;
                    log.info("清除客户端鉴权头:{}", name);
                    break;
                }
            }
            if (!isAuthHeader) {
                builder.addHeader(name, request.getHeader(name));
            }
        }
 
        // ========== 核心步骤2:注入代理侧STS凭证 ==========
        // 1. 添加STS临时令牌(MinIO STS必须)
        builder.addHeader(X_AMZ_SECURITY_TOKEN, credentials.getSecurityToken());
        // 2. 添加AK/SK(MinIO签名需要,透传给MinIO)
        builder.addHeader("X-Amz-Access-Key-Id", credentials.getAccessKeyId());
        builder.addHeader("X-Amz-Secret-Access-Key", credentials.getAccessKeySecret());
        // 3. 强制HTTP/1.1(避免协议兼容问题)
        builder.addHeader("Connection", "keep-alive");
        builder.addHeader("Content-Type", request.getContentType() == null ? "application/octet-stream" : request.getContentType());
 
        // ========== 构建请求体(分大小文件) ==========
        RequestBody body = null;
        if (!"GET".equalsIgnoreCase(method) && !"HEAD".equalsIgnoreCase(method)) {
            InputStream inputStream = request.getInputStream();
            long contentLength = request.getContentLengthLong();
 
            // 小文件:预读取到内存
            if (contentLength > 0 && contentLength <= MAX_MEMORY_SIZE) {
                log.info("小文件上传:预读取到内存,大小:{:.2f}MB", contentLength / 1024.0 / 1024.0);
                byte[] content = readInputStreamToBytes(inputStream);
                MediaType mediaType = MediaType.parse(request.getContentType() == null ? "application/octet-stream" : request.getContentType());
                body = RequestBody.create(content, mediaType);
            }
            // 大文件:流式上传
            else {
                log.info("大文件/未知长度上传:使用流式传输");
                body = new RequestBody() {
                    @Override
                    public MediaType contentType() {
                        return MediaType.parse(request.getContentType() == null ? "application/octet-stream" : request.getContentType());
                    }
 
                    @Override
                    public long contentLength() {
                        return contentLength;
                    }
 
                    @Override
                    public void writeTo(BufferedSink sink) throws IOException {
                        byte[] buffer = new byte[BUFFER_SIZE];
                        int len;
                        long totalWritten = 0;
                        try (InputStream in = inputStream) {
                            while ((len = in.read(buffer)) != -1) {
                                sink.write(buffer, 0, len);
                                totalWritten += len;
                                if (totalWritten % (1024 * 1024 * 100) == 0) {
                                    sink.flush();
                                    log.info("已上传:{:.2f}MB", totalWritten / 1024.0 / 1024.0);
                                }
                            }
                            sink.flush();
                        }
                    }
                };
            }
        }
 
        return builder.method(method, body).build();
    }
 
    /**
     * 读取InputStream到字节数组
     */
    private byte[] readInputStreamToBytes(InputStream inputStream) throws IOException {
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[BUFFER_SIZE];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            bos.flush();
            return bos.toByteArray();
        }
    }
 
    /**
     * 转发MinIO响应到客户端
     */
    private void forwardResponse(Request request, HttpServletResponse response) throws IOException {
        try (Response okResponse = httpClient.newCall(request).execute()) {
            // 配置响应头
            response.setStatus(okResponse.code());
            response.setCharacterEncoding("UTF-8");
            String contentType = okResponse.header("Content-Type");
            if (contentType != null) {
                response.setContentType(contentType);
            }
 
            // 透传响应头(排除Transfer-Encoding)
            Headers headers = okResponse.headers();
            for (String name : headers.names()) {
                if (!name.equalsIgnoreCase(TRANSFER_ENCODING)) {
                    response.setHeader(name, headers.get(name));
                }
            }
 
            // 透传响应体
            ResponseBody responseBody = okResponse.body();
            if (responseBody != null) {
                try (InputStream in = responseBody.byteStream()) {
                    byte[] buffer = new byte[BUFFER_SIZE];
                    int len;
                    while ((len = in.read(buffer)) != -1) {
                        response.getOutputStream().write(buffer, 0, len);
                    }
                    response.getOutputStream().flush();
                }
            }
        }
    }
 
    /**
     * 统一返回错误响应
     */
    private void sendError(HttpServletResponse response, int code, String msg) {
        try {
            response.sendError(code, msg);
        } catch (IOException e) {
            log.error("发送错误响应失败", e);
        }
    }
}