zhaowenyu
2026-02-04 9c3c085032bddf6bb5e8b730067292e7f1520328
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
package com.ard.web.controller.sample.media;
 
import com.ard.common.config.OssConfiguration;
import com.ard.oss.model.CredentialsDTO;
import com.ard.oss.service.impl.OssServiceContext;
import com.ard.sample.media.Response.S3ErrorResponse;
import com.ard.sample.media.Response.S3PutObjectResponse;
import io.minio.BucketExistsArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.credentials.StaticProvider;
import io.minio.errors.ErrorResponseException;
import io.minio.errors.MinioException;
import io.minio.messages.ErrorResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
 
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
 
/**
 * 适配 MinIO SDK 8.3.7 + 服务器 RELEASE.2024-08-03
 * 承接大疆 PUT 上传请求,中转上传到 MinIO HTTPS 接口
 */
@Api(tags = "机场上传minio")
@Slf4j
@RestController
@RequestMapping("/minioTransfer") // 根路径,匹配 Nginx 转发的 /dock/、/kmz/ 路径
public class MinioProxyUploadController {
 
    // 注入你的凭证服务(保持原有注入方式不变)
    @Autowired
    private OssServiceContext ossService;
 
    // S3标准错误码 -> HTTP状态码 映射表(行业通用标准)
    private static final Map<String, HttpStatus> S3_ERROR_CODE_MAP;
 
    // 初始化常见错误映射
    static {
        S3_ERROR_CODE_MAP = new HashMap<>();
        S3_ERROR_CODE_MAP.put("NoSuchBucket", HttpStatus.NOT_FOUND);
        S3_ERROR_CODE_MAP.put("NoSuchKey", HttpStatus.NOT_FOUND);
        S3_ERROR_CODE_MAP.put("AccessDenied", HttpStatus.FORBIDDEN);
        S3_ERROR_CODE_MAP.put("InvalidCredentials", HttpStatus.FORBIDDEN);
        S3_ERROR_CODE_MAP.put("BucketAlreadyExists", HttpStatus.CONFLICT);
        S3_ERROR_CODE_MAP.put("InvalidRequest", HttpStatus.BAD_REQUEST);
    }
 
    /**
     * 完全复刻 MinIO 原生 PUT 上传接口
     * 通配符匹配所有路径,保留原始URI格式
     */
    @ApiOperation("MinIO原生PUT接口代理(无感中转)")
    @PutMapping(value = "/{bucketName}/**", consumes = MediaType.ALL_VALUE)
    public ResponseEntity<?> proxyMinioPutObject(
            @PathVariable String bucketName,
            // 透传大疆设备原始请求体,不修改、不封装
            @RequestBody(required = false) byte[] rawRequestBody,
            HttpServletRequest request
    ) {
        log.info("接收参数bucketName:{}",bucketName);
        // ===================== 核心修复:安全路径解析,替换替代截取,杜绝索引错误 =====================
        // String contextPath = request.getContextPath();
        String rawUri = URLDecoder.decode(request.getRequestURI(), StandardCharsets.UTF_8);
        // 剔除上下文路径,得到纯业务路径
        String purePath = rawUri.substring("/minioTransfer".length());
        String bucketPrefix = "/" + bucketName;
 
        log.info("中转MinIO 路径解析开始 | 原始URI:{} | 纯业务路径:{} | 桶前缀:{}", rawUri, purePath, bucketPrefix);
 
        // 校验路径是否以桶前缀开头
        if (!purePath.startsWith(bucketPrefix)) {
            return buildNativeS3Error("InvalidRequest", "请求路径格式不匹配", rawUri, HttpStatus.BAD_REQUEST);
        }
 
        // 修复点:replace替换前缀,替代 substring 长度截取,彻底解决索引错位问题
        String objectKey = purePath.replaceFirst("^" + bucketPrefix, "");
        // 兜底处理空路径
        if (objectKey.isEmpty()) {
            objectKey = "/";
        }
 
        log.info("中转MinIO 路径解析成功 | 纯业务路径:{} | 桶名:{} | 最终对象路径:{}",
                purePath, bucketName, objectKey);
 
        // byte[] requestData = rawRequestBody == null ? new byte[0] : rawRequestBody;
 
        try {
            // ===================== 2. 复用原有逻辑:获取临时凭证 =====================
            var credentials = ossService.getCredentials();
            if (credentials == null || credentials.getAccessKeyId() == null || credentials.getSecurityToken() == null) {
                log.error("临时凭证获取失败");
                return buildNativeS3Error("InvalidCredentials", "临时凭证无效", rawUri, HttpStatus.FORBIDDEN);
            }
 
            // ===================== 3. 复用原有逻辑:构建MinIO客户端 =====================
            MinioClient minioClient = MinioClient.builder()
                    .endpoint(OssConfiguration.endpoint)
                    .credentialsProvider(new StaticProvider(
                            credentials.getAccessKeyId(),
                            credentials.getAccessKeySecret(),
                            credentials.getSecurityToken()
                    ))
                    .build();
 
            // 校验桶存在性(兼容原生接口行为,桶不存在返回404)
            boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!bucketExists) {
                return buildNativeS3Error("NoSuchBucket", "指定存储桶不存在", bucketName, HttpStatus.NOT_FOUND);
            }
 
            // ===================== 4. 透传请求:原样转发大疆原始数据到MinIO,无业务篡改 =====================
            // 空请求体=大疆原生创建目录请求,直接透传;带请求体=上传文件,原生处理
            byte[] requestBody = rawRequestBody == null ? new byte[0] : rawRequestBody;
            try (ByteArrayInputStream inputStream = new ByteArrayInputStream(requestBody)) {
                minioClient.putObject(
                        PutObjectArgs.builder()
                                .bucket(bucketName)
                                .object(objectKey)
                                // 透传原始流、原始长度,完全复刻原生请求
                                .stream(inputStream, requestBody.length, -1)
                                // 透传原生Content-Type,无修改
                                .contentType(request.getContentType() == null ? "application/octet-stream" : request.getContentType())
                                .build()
                );
            }
 
            // ===================== 5. 原生接口成功响应:200 OK + 空响应体(核心对齐!)=====================
            log.info("中转上传成功 | 路径:{}", objectKey);
            return ResponseEntity.ok()
                    // 无任何响应体,与MinIO原生行为完全一致
                    .body(null);
 
        } catch (ErrorResponseException e) {
            // ===================== 最终修正:无任何无效方法调用 =====================
            ErrorResponse nativeError = e.errorResponse();
            String errorCode = nativeError.code();
            // 映射状态码,无匹配则默认500
            HttpStatus status = S3_ERROR_CODE_MAP.getOrDefault(errorCode, HttpStatus.INTERNAL_SERVER_ERROR);
 
            log.error("MinIO服务错误 | 错误码:{} | 信息:{}", errorCode, nativeError.message());
            // 透传MinIO原生XML错误体,大疆设备完美兼容
            return ResponseEntity.status(status)
                    .contentType(MediaType.APPLICATION_XML)
                    .body(nativeError);
        } catch (MinioException e) {
            log.error("MinIO客户端异常:{}", e.getMessage());
            return buildNativeS3Error("ClientError", e.getMessage(), rawUri, HttpStatus.INTERNAL_SERVER_ERROR);
        } catch (Exception e) {
            log.error("代理中转通用异常:{}", e.getMessage(), e);
            return buildNativeS3Error("InternalError", "服务器代理异常", rawUri, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
 
    /**
     * 兜底构造S3标准XML错误响应,兼容大疆设备解析
     */
    private ResponseEntity<S3ErrorResponse> buildNativeS3Error(String code, String message, String resource, HttpStatus status) {
        S3ErrorResponse error = new S3ErrorResponse(code, message, resource);
        return ResponseEntity.status(status)
                .contentType(MediaType.APPLICATION_XML)
                .body(error);
    }
}