liudongzhi 2 месяцев назад
Родитель
Сommit
7399e5a29e

+ 27 - 0
alien-store/pom.xml

@@ -195,6 +195,29 @@
         </dependency>
         </dependency>
         <!--Swagger End-->
         <!--Swagger End-->
 
 
+
+        <!-- 阿里云 OSS 核心依赖 -->
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+            <version>3.16.0</version>
+        </dependency>
+        <!-- 用于 JSON 处理 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>2.0.32</version>
+        </dependency>
+        <!-- 用于 HMAC-SHA1 签名 -->
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>1.15</version>
+        </dependency>
+
+
+
+
         <dependency>
         <dependency>
             <groupId>org.projectlombok</groupId>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
             <artifactId>lombok</artifactId>
@@ -313,6 +336,10 @@
             <groupId>jakarta.validation</groupId>
             <groupId>jakarta.validation</groupId>
             <artifactId>jakarta.validation-api</artifactId>
             <artifactId>jakarta.validation-api</artifactId>
         </dependency>
         </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+        </dependency>
     </dependencies>
     </dependencies>
 
 
     <build>
     <build>

+ 4 - 0
alien-store/src/main/java/shop/alien/store/controller/OSSDirectUploadController.java

@@ -9,8 +9,12 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.result.R;
 import shop.alien.store.util.oss.OSSDirectUploadUtil;
 import shop.alien.store.util.oss.OSSDirectUploadUtil;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
 
 
 import java.util.List;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
 /**
 /**

+ 273 - 0
alien-store/src/main/java/shop/alien/store/controller/OSSDirectUploadNewController.java

@@ -0,0 +1,273 @@
+package shop.alien.store.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.aliyun.oss.common.utils.BinaryUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * OSS直传新版本Controller(使用OSS4-HMAC-SHA256签名方式)
+ * 
+ * @author system
+ * @date 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"OSS直传新版本接口"})
+@RestController
+@RequestMapping("/oss/direct/new")
+@RefreshScope
+public class OSSDirectUploadNewController {
+
+    @Value("${ali.oss.accessKeyId}")
+    private String accessKeyId;
+
+    @Value("${ali.oss.accessKeySecret}")
+    private String accessKeySecret;
+
+    @Value("${ali.oss.endPoint}")
+    private String endPoint;
+
+    @Value("${ali.oss.bucketName}")
+    private String bucketName;
+
+    // 从endPoint提取region(例如:oss-cn-beijing.aliyuncs.com -> cn-beijing)
+    private String getRegion() {
+        if (endPoint != null && endPoint.contains("oss-")) {
+            int start = endPoint.indexOf("oss-") + 4;
+            int end = endPoint.indexOf(".aliyuncs.com");
+            if (end > start) {
+                return endPoint.substring(start, end);
+            }
+        }
+        return "cn-beijing"; // 默认值
+    }
+
+    // 构建host地址
+    private String getHost() {
+        return "https://" + bucketName + "." + endPoint;
+    }
+
+    // 设置上传回调URL(可选,如果需要回调功能)
+    @Value("${ali.oss.callbackUrl:}")
+    private String callbackUrl;
+
+    // 限定上传到OSS的文件前缀(默认值)
+    @Value("${ali.oss.uploadDir:upload/}")
+    private String uploadDir;
+
+    // 指定过期时间,单位为秒(默认1小时)
+    @Value("${ali.oss.expireTime:3600}")
+    private Long expireTime;
+
+    /**
+     * 通过指定有效的时长(秒)生成过期时间。
+     * @param seconds 有效时长(秒)。
+     * @return ISO8601 时间字符串,如:"2014-12-01T12:00:00.000Z"。
+     */
+    public static String generateExpiration(long seconds) {
+        // 获取当前时间戳(以秒为单位)
+        long now = Instant.now().getEpochSecond();
+        // 计算过期时间的时间戳
+        long expirationTime = now + seconds;
+        // 将时间戳转换为Instant对象,并格式化为ISO8601格式
+        Instant instant = Instant.ofEpochSecond(expirationTime);
+        // 定义时区为UTC
+        ZoneId zone = ZoneOffset.UTC;
+        // 将 Instant 转换为 ZonedDateTime
+        ZonedDateTime zonedDateTime = instant.atZone(zone);
+        // 定义日期时间格式,例如2023-12-03T13:00:00.000Z
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+        // 格式化日期时间
+        String formattedDate = zonedDateTime.format(formatter);
+        // 输出结果
+        return formattedDate;
+    }
+    //初始化STS Client(如果需要使用STS,取消注释并配置相关环境变量)
+    /*
+    public static com.aliyun.sts20150401.Client createStsClient() throws Exception {
+        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
+        // 建议使用更安全的 STS 方式。
+        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
+                // 必填,请确保代码运行环境设置了环境变量 OSS_ACCESS_KEY_ID。
+                .setAccessKeyId(System.getenv("OSS_ACCESS_KEY_ID"))
+                // 必填,请确保代码运行环境设置了环境变量 OSS_ACCESS_KEY_SECRET。
+                .setAccessKeySecret(System.getenv("OSS_ACCESS_KEY_SECRET"));
+        // Endpoint 请参考 https://api.aliyun.com/product/Sts
+        config.endpoint = "sts.cn-hangzhou.aliyuncs.com";
+        return new com.aliyun.sts20150401.Client(config);
+    }
+    */
+
+    //获取STS临时凭证
+    // 注意:此方法需要STS服务支持,如果不需要STS,可以直接使用AccessKey
+    // 这里暂时注释掉STS相关代码,直接使用配置的AccessKey
+    /*
+    public static AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials getCredential() throws Exception {
+        com.aliyun.sts20150401.Client client = OSSDirectUploadNewController.createStsClient();
+        com.aliyun.sts20150401.models.AssumeRoleRequest assumeRoleRequest = new com.aliyun.sts20150401.models.AssumeRoleRequest()
+                // 必填,请确保代码运行环境设置了环境变量 OSS_STS_ROLE_ARN
+                .setRoleArn(System.getenv("OSS_STS_ROLE_ARN"))
+                .setRoleSessionName("yourRoleSessionName");// 自定义会话名称
+        com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
+        try {
+            // 复制代码运行请自行打印 API 的返回值
+            AssumeRoleResponse response = client.assumeRoleWithOptions(assumeRoleRequest, runtime);
+            // credentials里包含了后续要用到的AccessKeyId、AccessKeySecret和SecurityToken。
+            return response.body.credentials;
+        } catch (Exception error) {
+            log.error("获取STS凭证失败: {}", error.getMessage(), error);
+            throw new RuntimeException("获取STS凭证失败", error);
+        }
+        return null;
+    }
+    */
+
+    @ApiOperation("生成OSS直传签名(OSS4-HMAC-SHA256方式)")
+    @GetMapping("/signature")
+    public ResponseEntity<Map<String, String>> getPostSignatureForOssUpload() {
+        try {
+            // 直接使用配置的AccessKey(如果使用STS,需要调用getCredential()方法)
+            String accesskeyid = accessKeyId;
+            String accesskeysecret = accessKeySecret;
+            String securitytoken = ""; // 如果使用STS,这里应该是securityToken
+            
+            String region = getRegion();
+            String host = getHost();
+
+        //获取x-oss-credential里的date,当前日期,格式为yyyyMMdd
+        ZonedDateTime today = ZonedDateTime.now().withZoneSameInstant(java.time.ZoneOffset.UTC);
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
+        String date = today.format(formatter);
+
+        //获取x-oss-date
+        ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(java.time.ZoneOffset.UTC);
+        DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");
+        String x_oss_date = now.format(formatter2);
+
+        // 步骤1:创建policy。
+        String x_oss_credential = accesskeyid + "/" + date + "/" + region + "/oss/aliyun_v4_request";
+
+        ObjectMapper mapper = new ObjectMapper();
+
+        Map<String, Object> policy = new HashMap<>();
+        policy.put("expiration", generateExpiration(expireTime));
+
+        List<Object> conditions = new ArrayList<>();
+
+        Map<String, String> bucketCondition = new HashMap<>();
+        bucketCondition.put("bucket", bucketName);
+        conditions.add(bucketCondition);
+
+        Map<String, String> signatureVersionCondition = new HashMap<>();
+        signatureVersionCondition.put("x-oss-signature-version", "OSS4-HMAC-SHA256");
+        conditions.add(signatureVersionCondition);
+
+        Map<String, String> credentialCondition = new HashMap<>();
+        credentialCondition.put("x-oss-credential", x_oss_credential); // 替换为实际的 access key id
+        conditions.add(credentialCondition);
+
+        Map<String, String> dateCondition = new HashMap<>();
+        dateCondition.put("x-oss-date", x_oss_date);
+        conditions.add(dateCondition);
+
+        // 如果使用STS,需要添加security-token条件
+        if (securitytoken != null && !securitytoken.isEmpty()) {
+            Map<String, String> securityTokenCondition = new HashMap<>();
+            securityTokenCondition.put("x-oss-security-token", securitytoken);
+            conditions.add(securityTokenCondition);
+        }
+
+        conditions.add(Arrays.asList("content-length-range", 1, 10240000));
+        conditions.add(Arrays.asList("eq", "$success_action_status", "200"));
+        conditions.add(Arrays.asList("starts-with", "$key", uploadDir));
+
+        policy.put("conditions", conditions);
+
+        String jsonPolicy = mapper.writeValueAsString(policy);
+
+        // 步骤2:构造待签名字符串(StringToSign)。
+        String stringToSign = new String(Base64.encodeBase64(jsonPolicy.getBytes()));
+
+        // 步骤3:计算SigningKey。
+        byte[] dateKey = hmacsha256(("aliyun_v4" + accesskeysecret).getBytes(), date);
+        byte[] dateRegionKey = hmacsha256(dateKey, region);
+        byte[] dateRegionServiceKey = hmacsha256(dateRegionKey, "oss");
+        byte[] signingKey = hmacsha256(dateRegionServiceKey, "aliyun_v4_request");
+
+        // 步骤4:计算Signature。
+        byte[] result = hmacsha256(signingKey, stringToSign);
+        String signature = BinaryUtil.toHex(result);
+
+        // 步骤5:设置回调(可选)。
+        String base64CallbackBody = "";
+        if (callbackUrl != null && !callbackUrl.isEmpty()) {
+            JSONObject jasonCallback = new JSONObject();
+            jasonCallback.put("callbackUrl", callbackUrl);
+            jasonCallback.put("callbackBody", "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
+            jasonCallback.put("callbackBodyType", "application/x-www-form-urlencoded");
+            base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes(StandardCharsets.UTF_8));
+        }
+
+        Map<String, String> response = new HashMap<>();
+        // 将数据添加到 map 中
+        response.put("version", "OSS4-HMAC-SHA256");
+        // 这里是易错点,不能直接传policy,需要做一下Base64编码
+        response.put("policy", stringToSign);
+        response.put("x_oss_credential", x_oss_credential);
+        response.put("x_oss_date", x_oss_date);
+        response.put("signature", signature);
+        // 只有在使用STS时才添加security_token
+        if (securitytoken != null && !securitytoken.isEmpty()) {
+            response.put("security_token", securitytoken);
+        }
+        response.put("dir", uploadDir);
+        response.put("host", host);
+        if (!base64CallbackBody.isEmpty()) {
+            response.put("callback", base64CallbackBody);
+        }
+        
+        log.info("生成OSS直传签名成功(OSS4方式): dir={}, host={}", uploadDir, host);
+        // 返回带有状态码 200 (OK) 的 ResponseEntity,返回给Web端,进行PostObject操作
+        return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            log.error("生成OSS直传签名失败(OSS4方式): {}", e.getMessage(), e);
+            Map<String, String> errorResponse = new HashMap<>();
+            errorResponse.put("error", "生成签名失败: " + e.getMessage());
+            return ResponseEntity.status(500).body(errorResponse);
+        }
+    }
+    public static byte[] hmacsha256(byte[] key, String data) {
+        try {
+            // 初始化HMAC密钥规格,指定算法为HMAC-SHA256并使用提供的密钥。
+            SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");
+
+            // 获取Mac实例,并通过getInstance方法指定使用HMAC-SHA256算法。
+            Mac mac = Mac.getInstance("HmacSHA256");
+            // 使用密钥初始化Mac对象。
+            mac.init(secretKeySpec);
+
+            // 执行HMAC计算,通过doFinal方法接收需要计算的数据并返回计算结果的数组。
+            byte[] hmacBytes = mac.doFinal(data.getBytes());
+
+            return hmacBytes;
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to calculate HMAC-SHA256", e);
+        }
+    }
+}

+ 201 - 15
alien-store/src/main/java/shop/alien/store/util/oss/OSSDirectUploadUtil.java

@@ -1,6 +1,9 @@
 package shop.alien.store.util.oss;
 package shop.alien.store.util.oss;
 
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.annotation.JSONField;
 import com.aliyun.oss.OSS;
 import com.aliyun.oss.OSS;
 import com.aliyun.oss.OSSClientBuilder;
 import com.aliyun.oss.OSSClientBuilder;
 import com.aliyun.oss.common.comm.SignVersion;
 import com.aliyun.oss.common.comm.SignVersion;
@@ -82,15 +85,89 @@ public class OSSDirectUploadUtil {
             Date expiration = new Date(expire * 1000);
             Date expiration = new Date(expire * 1000);
             
             
             // 构建Post Policy
             // 构建Post Policy
-            String policy = buildPostPolicy(bucketName, ossKey, maxSize, expiration);
+            // 使用 JSONObject 和 JSONArray 确保格式完全符合 OSS 规范
+            JSONObject policyObj = new JSONObject();
             
             
+            // 1. 设置过期时间(必须未来时间,ISO8601 格式,UTC 时区)
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+            sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+            String expirationStr = sdf.format(expiration);
+            policyObj.put("expiration", expirationStr);
+            
+            // 2. 构建 conditions 数组(必须严格按照 OSS 规范)
+            // OSS 支持的匹配方式:content-length-range, eq, starts-with, in, not-in
+            // 注意:不支持 ends-with 等其他匹配方式
+            JSONArray conditions = new JSONArray();
+            
+            // bucket 条件:["eq", "$bucket", "bucket-name"](必须使用 eq 和 $bucket)
+            JSONArray bucketCondition = new JSONArray();
+            bucketCondition.add("eq");
+            bucketCondition.add("$bucket");
+            bucketCondition.add(bucketName);
+            conditions.add(bucketCondition);
+            
+            // key 条件:["starts-with", "$key", "dir/"](前缀匹配,允许上传到指定目录下的任意文件)
+            // 使用 starts-with 而不是 eq,这样前端可以上传任意文件名到指定目录
+            // 注意:dir 已经确保以 "/" 结尾
+            JSONArray keyCondition = new JSONArray();
+            keyCondition.add("starts-with");
+            keyCondition.add("$key");
+            keyCondition.add(dir);  // 使用目录前缀,如 "uploads/report/"
+            conditions.add(keyCondition);
+            
+            // 文件大小范围:["content-length-range", "0", "maxSize"]
+            JSONArray sizeCondition = new JSONArray();
+            sizeCondition.add("content-length-range");
+            sizeCondition.add("0");
+            sizeCondition.add(String.valueOf(maxSize));
+            conditions.add(sizeCondition);
+            
+            // 成功返回状态码:["eq", "$success_action_status", "200"](可选,但推荐)
+            JSONArray statusCondition = new JSONArray();
+            statusCondition.add("eq");
+            statusCondition.add("$success_action_status");
+            statusCondition.add("200");
+            conditions.add(statusCondition);
+            
+            policyObj.put("conditions", conditions);
+            
+            // 3. 转换为 JSON 字符串(使用 toJSONString 确保格式正确)
+            String policyJson = policyObj.toJSONString();
+            
+            // 4. 验证 JSON 是否有效
+            try {
+                JSON.parseObject(policyJson);
+                log.info("Policy JSON 验证通过: {}", policyJson);
+            } catch (Exception e) {
+                log.error("Policy JSON 格式无效: {}", policyJson, e);
+                throw new RuntimeException("生成的 Policy JSON 格式无效", e);
+            }
+            
+            // 5. Base64 编码(确保没有换行符)
+            String policyBase64 = BinaryUtil.toBase64String(policyJson.getBytes(StandardCharsets.UTF_8));
+            // 移除可能的换行符(Base64 编码不应该包含换行符)
+            policyBase64 = policyBase64.replaceAll("\\s+", "");
+            log.info("Policy Base64: {}", policyBase64);
+            
+            // 6. 验证 Base64 解码后是否为合法 JSON
+            try {
+                String decoded = new String(java.util.Base64.getDecoder().decode(policyBase64), StandardCharsets.UTF_8);
+                JSON.parseObject(decoded);
+                log.info("Base64 解码验证通过,解码后的 JSON: {}", decoded);
+            } catch (Exception e) {
+                log.error("Base64 解码后不是合法 JSON,原始 Base64: {}", policyBase64, e);
+                throw new RuntimeException("Base64 解码后不是合法 JSON", e);
+            }
+
+
+
             // 计算签名
             // 计算签名
-            String signature = calculateSignature(policy);
+            String signature = calculateSignature(policyBase64);
             
             
             // 构建返回结果
             // 构建返回结果
             OSSSignatureResult result = new OSSSignatureResult();
             OSSSignatureResult result = new OSSSignatureResult();
             result.setAccessKeyId(accessKeyId);
             result.setAccessKeyId(accessKeyId);
-            result.setPolicy(policy);
+            result.setPolicy(policyBase64);
             result.setSignature(signature);
             result.setSignature(signature);
             result.setDir(dir);
             result.setDir(dir);
             result.setHost("https://" + bucketName + "." + endPoint);
             result.setHost("https://" + bucketName + "." + endPoint);
@@ -99,7 +176,10 @@ public class OSSDirectUploadUtil {
             result.setCallbackUrl(""); // 回调URL(可选)
             result.setCallbackUrl(""); // 回调URL(可选)
             result.setCallbackBody(""); // 回调Body(可选)
             result.setCallbackBody(""); // 回调Body(可选)
             
             
+            // 输出完整的签名信息(用于调试)
             log.info("生成OSS直传签名成功: dir={}, fileName={}, ossKey={}", dir, fileName, ossKey);
             log.info("生成OSS直传签名成功: dir={}, fileName={}, ossKey={}", dir, fileName, ossKey);
+            log.info("签名信息 - Policy长度: {}, Signature: {}", policyBase64.length(), signature);
+            
             return result;
             return result;
             
             
         } catch (Exception e) {
         } catch (Exception e) {
@@ -109,25 +189,86 @@ public class OSSDirectUploadUtil {
     }
     }
 
 
     /**
     /**
-     * 构建Post Policy JSON
+     * 构建 Post Policy JSON
+     * 按照 OSS 规范:先生成合法的 JSON 字符串,再进行 Base64 编码
+     * 使用 JSONObject 和 JSONArray 手动构建,确保格式完全符合 OSS 要求
      */
      */
     private String buildPostPolicy(String bucket, String key, long maxSize, Date expiration) {
     private String buildPostPolicy(String bucket, String key, long maxSize, Date expiration) {
         try {
         try {
-            Map<String, Object> policyMap = new LinkedHashMap<>();
-            policyMap.put("expiration", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").format(expiration));
+            // 1. 设置过期时间(必须未来时间,ISO8601 格式,UTC 时区)
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+            sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+            String expirationStr = sdf.format(expiration);
+            
+            // 2. 使用 JSONObject 和 JSONArray 手动构建 Policy JSON
+            // 这样可以确保格式完全符合 OSS 规范,避免 FastJSON 自动序列化的问题
+            JSONObject policyObj = new JSONObject();
+            policyObj.put("expiration", expirationStr);
+            
+            // 3. 构建 conditions 数组(必须严格按照 OSS 规范)
+            JSONArray conditions = new JSONArray();
+            
+            // bucket 条件:["eq", "$bucket", "bucket-name"]
+            JSONArray bucketCondition = new JSONArray();
+            bucketCondition.add("eq");
+            bucketCondition.add("$bucket");
+            bucketCondition.add(bucket);
+            conditions.add(bucketCondition);
+            
+            // key 条件:["eq", "$key", "file-path"]
+            JSONArray keyCondition = new JSONArray();
+            keyCondition.add("eq");
+            keyCondition.add("$key");
+            keyCondition.add(key);
+            conditions.add(keyCondition);
+            
+            // 文件大小范围:["content-length-range", "0", "maxSize"]
+            // 注意:OSS 要求所有参数都是字符串格式
+            JSONArray sizeCondition = new JSONArray();
+            sizeCondition.add("content-length-range");
+            sizeCondition.add("0");
+            sizeCondition.add(String.valueOf(maxSize));
+            conditions.add(sizeCondition);
+            
+            policyObj.put("conditions", conditions);
             
             
-            List<Object> conditions = new ArrayList<>();
-            conditions.add(new String[]{"eq", "$bucket", bucket});
-            conditions.add(new String[]{"eq", "$key", key});
-            conditions.add(new String[]{"content-length-range", String.valueOf(0), String.valueOf(maxSize)});
+            // 4. 转换为 JSON 字符串(使用 toJSONString 确保格式正确)
+            String policyJson = policyObj.toJSONString();
             
             
-            policyMap.put("conditions", conditions);
+            // 5. 验证 JSON 是否有效(用于调试)
+            try {
+                JSON.parseObject(policyJson);
+                log.info("Policy JSON 验证通过: {}", policyJson);
+            } catch (Exception e) {
+                log.error("Policy JSON 格式无效: {}", policyJson, e);
+                throw new RuntimeException("生成的 Policy JSON 格式无效", e);
+            }
+            
+            // 6. Base64 编码后返回
+            String policyBase64 = BinaryUtil.toBase64String(policyJson.getBytes(StandardCharsets.UTF_8));
+            log.info("Policy Base64: {}", policyBase64);
+            
+            // 7. 验证 Base64 解码后是否为合法 JSON(用于调试)
+            try {
+                String decoded = new String(java.util.Base64.getDecoder().decode(policyBase64), StandardCharsets.UTF_8);
+                JSON.parseObject(decoded);
+                log.info("Base64 解码验证通过,解码后的 JSON: {}", decoded);
+            } catch (Exception e) {
+                log.error("Base64 解码后不是合法 JSON,原始 Base64: {}", policyBase64, e);
+                // 尝试手动解码查看内容
+                try {
+                    String decoded = new String(java.util.Base64.getDecoder().decode(policyBase64), StandardCharsets.UTF_8);
+                    log.error("Base64 解码后的内容: {}", decoded);
+                } catch (Exception decodeEx) {
+                    log.error("无法解码 Base64: {}", decodeEx.getMessage());
+                }
+                throw new RuntimeException("Base64 解码后不是合法 JSON", e);
+            }
             
             
-            String policyJson = JSON.toJSONString(policyMap);
-            return BinaryUtil.toBase64String(policyJson.getBytes(StandardCharsets.UTF_8));
+            return policyBase64;
         } catch (Exception e) {
         } catch (Exception e) {
-            log.error("构建Post Policy失败: {}", e.getMessage(), e);
-            throw new RuntimeException("构建Post Policy失败", e);
+            log.error("构建 Post Policy 失败:{}", e.getMessage(), e);
+            throw new RuntimeException("构建 Post Policy 失败: " + e.getMessage(), e);
         }
         }
     }
     }
 
 
@@ -350,6 +491,30 @@ public class OSSDirectUploadUtil {
         }
         }
     }
     }
 
 
+
+
+    /**
+     * 生成 HMAC-SHA1 签名
+     * @param data 待签名数据(Policy Base64)
+     * @param secretKey 秘钥(AccessKeySecret)
+     * @return 签名结果(Base64 编码)
+     */
+    private static String generateHmacSHA1Signature(String data, String secretKey) throws Exception {
+        Mac mac = Mac.getInstance("HmacSHA1");
+        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA1");
+        mac.init(secretKeySpec);
+        byte[] signBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
+        return BinaryUtil.toBase64String(signBytes);
+    }
+
+
+
+
+
+
+
+
+
     /**
     /**
      * 创建OSS客户端
      * 创建OSS客户端
      */
      */
@@ -362,16 +527,37 @@ public class OSSDirectUploadUtil {
     /**
     /**
      * OSS签名结果
      * OSS签名结果
      */
      */
+    /**
+     * OSS签名结果
+     * 注意:使用 @JSONField 确保字段名正确序列化
+     */
     @Data
     @Data
     public static class OSSSignatureResult {
     public static class OSSSignatureResult {
+        @JSONField(name = "accessKeyId")
         private String accessKeyId;
         private String accessKeyId;
+        
+        @JSONField(name = "policy")
         private String policy;
         private String policy;
+        
+        @JSONField(name = "signature")
         private String signature;
         private String signature;
+        
+        @JSONField(name = "dir")
         private String dir;
         private String dir;
+        
+        @JSONField(name = "host")
         private String host;
         private String host;
+        
+        @JSONField(name = "expire")
         private String expire;
         private String expire;
+        
+        @JSONField(name = "ossKey")
         private String ossKey;
         private String ossKey;
+        
+        @JSONField(name = "callbackUrl")
         private String callbackUrl;
         private String callbackUrl;
+        
+        @JSONField(name = "callbackBody")
         private String callbackBody;
         private String callbackBody;
     }
     }
 }
 }

+ 24 - 0
alien-util/pom.xml

@@ -361,6 +361,30 @@
             <version>3.17.4</version>
             <version>3.17.4</version>
         </dependency>
         </dependency>
 
 
+
+        <!-- 阿里云 OSS 核心依赖 -->
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+            <version>3.16.0</version>
+        </dependency>
+        <!-- 用于 JSON 处理 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>2.0.32</version>
+        </dependency>
+        <!-- 用于 HMAC-SHA1 签名 -->
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>1.15</version>
+        </dependency>
+
+
+
+
+
         <dependency>
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-context</artifactId>
             <artifactId>spring-cloud-context</artifactId>