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