|
|
@@ -0,0 +1,377 @@
|
|
|
+package shop.alien.store.util.oss;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.aliyun.oss.OSS;
|
|
|
+import com.aliyun.oss.OSSClientBuilder;
|
|
|
+import com.aliyun.oss.common.comm.SignVersion;
|
|
|
+import com.aliyun.oss.common.utils.BinaryUtil;
|
|
|
+import com.aliyun.oss.model.*;
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
|
|
+import lombok.Data;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.cloud.context.config.annotation.RefreshScope;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import javax.crypto.Mac;
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * OSS直传工具类
|
|
|
+ * 支持:签名生成、分片上传、断点续传、上传进度
|
|
|
+ *
|
|
|
+ * @author system
|
|
|
+ * @date 2025-01-XX
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+@RefreshScope
|
|
|
+public class OSSDirectUploadUtil {
|
|
|
+
|
|
|
+ @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;
|
|
|
+
|
|
|
+ // 默认过期时间:1小时
|
|
|
+ private static final long DEFAULT_EXPIRE_TIME = 3600 * 1000L;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成OSS直传签名(Post Policy方式)
|
|
|
+ *
|
|
|
+ * @param dir 上传目录(如:video/、image/)
|
|
|
+ * @param fileName 文件名(可选,不传则自动生成)
|
|
|
+ * @param maxSize 最大文件大小(字节,默认100MB)
|
|
|
+ * @param expireTime 过期时间(毫秒,默认1小时)
|
|
|
+ * @return 签名信息
|
|
|
+ */
|
|
|
+ public OSSSignatureResult generatePostSignature(String dir, String fileName, Long maxSize, Long expireTime) {
|
|
|
+ try {
|
|
|
+ // 参数校验和默认值
|
|
|
+ if (StringUtils.isEmpty(dir)) {
|
|
|
+ dir = "upload/";
|
|
|
+ }
|
|
|
+ if (!dir.endsWith("/")) {
|
|
|
+ dir += "/";
|
|
|
+ }
|
|
|
+ if (StringUtils.isEmpty(fileName)) {
|
|
|
+ fileName = UUID.randomUUID().toString();
|
|
|
+ }
|
|
|
+ if (maxSize == null || maxSize <= 0) {
|
|
|
+ maxSize = 100 * 1024 * 1024L; // 默认100MB
|
|
|
+ }
|
|
|
+ if (expireTime == null || expireTime <= 0) {
|
|
|
+ expireTime = DEFAULT_EXPIRE_TIME;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成OSS文件路径
|
|
|
+ String ossKey = dir + fileName;
|
|
|
+
|
|
|
+ // 过期时间(Unix时间戳,秒)
|
|
|
+ long expire = (System.currentTimeMillis() + expireTime) / 1000;
|
|
|
+ Date expiration = new Date(expire * 1000);
|
|
|
+
|
|
|
+ // 构建Post Policy
|
|
|
+ String policy = buildPostPolicy(bucketName, ossKey, maxSize, expiration);
|
|
|
+
|
|
|
+ // 计算签名
|
|
|
+ String signature = calculateSignature(policy);
|
|
|
+
|
|
|
+ // 构建返回结果
|
|
|
+ OSSSignatureResult result = new OSSSignatureResult();
|
|
|
+ result.setAccessKeyId(accessKeyId);
|
|
|
+ result.setPolicy(policy);
|
|
|
+ result.setSignature(signature);
|
|
|
+ result.setDir(dir);
|
|
|
+ result.setHost("https://" + bucketName + "." + endPoint);
|
|
|
+ result.setExpire(String.valueOf(expire));
|
|
|
+ result.setOssKey(ossKey);
|
|
|
+ result.setCallbackUrl(""); // 回调URL(可选)
|
|
|
+ result.setCallbackBody(""); // 回调Body(可选)
|
|
|
+
|
|
|
+ log.info("生成OSS直传签名成功: dir={}, fileName={}, ossKey={}", dir, fileName, ossKey);
|
|
|
+ return result;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("生成OSS直传签名失败: {}", e.getMessage(), e);
|
|
|
+ throw new RuntimeException("生成OSS签名失败: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建Post Policy JSON
|
|
|
+ */
|
|
|
+ private String buildPostPolicy(String bucket, String key, long maxSize, Date expiration) {
|
|
|
+ try {
|
|
|
+ Map<String, Object> policyMap = new LinkedHashMap<>();
|
|
|
+ policyMap.put("expiration", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").format(expiration));
|
|
|
+
|
|
|
+ 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)});
|
|
|
+
|
|
|
+ policyMap.put("conditions", conditions);
|
|
|
+
|
|
|
+ String policyJson = JSON.toJSONString(policyMap);
|
|
|
+ return BinaryUtil.toBase64String(policyJson.getBytes(StandardCharsets.UTF_8));
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("构建Post Policy失败: {}", e.getMessage(), e);
|
|
|
+ throw new RuntimeException("构建Post Policy失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算签名
|
|
|
+ */
|
|
|
+ private String calculateSignature(String policy) {
|
|
|
+ try {
|
|
|
+ Mac hmac = Mac.getInstance("HmacSHA1");
|
|
|
+ hmac.init(new SecretKeySpec(accessKeySecret.getBytes(StandardCharsets.UTF_8), "HmacSHA1"));
|
|
|
+ byte[] signData = hmac.doFinal(policy.getBytes(StandardCharsets.UTF_8));
|
|
|
+ return BinaryUtil.toBase64String(signData);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("计算签名失败: {}", e.getMessage(), e);
|
|
|
+ throw new RuntimeException("计算签名失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化分片上传
|
|
|
+ *
|
|
|
+ * @param ossKey OSS文件路径
|
|
|
+ * @return 上传ID
|
|
|
+ */
|
|
|
+ public String initMultipartUpload(String ossKey) {
|
|
|
+ OSS ossClient = null;
|
|
|
+ try {
|
|
|
+ ossClient = createOSSClient();
|
|
|
+ InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, ossKey);
|
|
|
+ InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(request);
|
|
|
+ String uploadId = result.getUploadId();
|
|
|
+ log.info("初始化分片上传成功: ossKey={}, uploadId={}", ossKey, uploadId);
|
|
|
+ return uploadId;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("初始化分片上传失败: ossKey={}, error={}", ossKey, e.getMessage(), e);
|
|
|
+ throw new RuntimeException("初始化分片上传失败: " + e.getMessage(), e);
|
|
|
+ } finally {
|
|
|
+ if (ossClient != null) {
|
|
|
+ ossClient.shutdown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传分片
|
|
|
+ *
|
|
|
+ * @param ossKey OSS文件路径
|
|
|
+ * @param uploadId 上传ID
|
|
|
+ * @param partNumber 分片序号(从1开始)
|
|
|
+ * @param partData 分片数据
|
|
|
+ * @return 分片ETag
|
|
|
+ */
|
|
|
+ public String uploadPart(String ossKey, String uploadId, int partNumber, byte[] partData) {
|
|
|
+ OSS ossClient = null;
|
|
|
+ try {
|
|
|
+ ossClient = createOSSClient();
|
|
|
+ UploadPartRequest uploadPartRequest = new UploadPartRequest();
|
|
|
+ uploadPartRequest.setBucketName(bucketName);
|
|
|
+ uploadPartRequest.setKey(ossKey);
|
|
|
+ uploadPartRequest.setUploadId(uploadId);
|
|
|
+ uploadPartRequest.setPartNumber(partNumber);
|
|
|
+ uploadPartRequest.setInputStream(new java.io.ByteArrayInputStream(partData));
|
|
|
+ uploadPartRequest.setPartSize(partData.length);
|
|
|
+
|
|
|
+ UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
|
|
|
+ String eTag = uploadPartResult.getETag();
|
|
|
+ log.debug("上传分片成功: ossKey={}, partNumber={}, eTag={}", ossKey, partNumber, eTag);
|
|
|
+ return eTag;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("上传分片失败: ossKey={}, partNumber={}, error={}", ossKey, partNumber, e.getMessage(), e);
|
|
|
+ throw new RuntimeException("上传分片失败: " + e.getMessage(), e);
|
|
|
+ } finally {
|
|
|
+ if (ossClient != null) {
|
|
|
+ ossClient.shutdown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 完成分片上传(合并所有分片)
|
|
|
+ *
|
|
|
+ * @param ossKey OSS文件路径
|
|
|
+ * @param uploadId 上传ID
|
|
|
+ * @param partETags 所有分片的ETag列表
|
|
|
+ * @return 文件URL
|
|
|
+ */
|
|
|
+ public String completeMultipartUpload(String ossKey, String uploadId, List<PartETag> partETags) {
|
|
|
+ OSS ossClient = null;
|
|
|
+ try {
|
|
|
+ ossClient = createOSSClient();
|
|
|
+ CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
|
|
|
+ bucketName, ossKey, uploadId, partETags);
|
|
|
+ CompleteMultipartUploadResult completeResult = ossClient.completeMultipartUpload(completeRequest);
|
|
|
+
|
|
|
+ String fileUrl = "https://" + bucketName + "." + endPoint + "/" + ossKey;
|
|
|
+ log.info("完成分片上传: ossKey={}, uploadId={}, fileUrl={}", ossKey, uploadId, fileUrl);
|
|
|
+ return fileUrl;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("完成分片上传失败: ossKey={}, uploadId={}, error={}", ossKey, uploadId, e.getMessage(), e);
|
|
|
+ throw new RuntimeException("完成分片上传失败: " + e.getMessage(), e);
|
|
|
+ } finally {
|
|
|
+ if (ossClient != null) {
|
|
|
+ ossClient.shutdown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 取消分片上传(清理未完成的分片)
|
|
|
+ *
|
|
|
+ * @param ossKey OSS文件路径
|
|
|
+ * @param uploadId 上传ID
|
|
|
+ */
|
|
|
+ public void abortMultipartUpload(String ossKey, String uploadId) {
|
|
|
+ OSS ossClient = null;
|
|
|
+ try {
|
|
|
+ ossClient = createOSSClient();
|
|
|
+ AbortMultipartUploadRequest abortRequest = new AbortMultipartUploadRequest(bucketName, ossKey, uploadId);
|
|
|
+ ossClient.abortMultipartUpload(abortRequest);
|
|
|
+ log.info("取消分片上传: ossKey={}, uploadId={}", ossKey, uploadId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("取消分片上传失败: ossKey={}, uploadId={}, error={}", ossKey, uploadId, e.getMessage(), e);
|
|
|
+ } finally {
|
|
|
+ if (ossClient != null) {
|
|
|
+ ossClient.shutdown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 列出已上传的分片
|
|
|
+ *
|
|
|
+ * @param ossKey OSS文件路径
|
|
|
+ * @param uploadId 上传ID
|
|
|
+ * @return 已上传的分片列表
|
|
|
+ */
|
|
|
+ public List<PartSummary> listParts(String ossKey, String uploadId) {
|
|
|
+ OSS ossClient = null;
|
|
|
+ try {
|
|
|
+ ossClient = createOSSClient();
|
|
|
+ ListPartsRequest listPartsRequest = new ListPartsRequest(bucketName, ossKey, uploadId);
|
|
|
+ PartListing partListing = ossClient.listParts(listPartsRequest);
|
|
|
+ return partListing.getParts();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("列出分片失败: ossKey={}, uploadId={}, error={}", ossKey, uploadId, e.getMessage(), e);
|
|
|
+ throw new RuntimeException("列出分片失败: " + e.getMessage(), e);
|
|
|
+ } finally {
|
|
|
+ if (ossClient != null) {
|
|
|
+ ossClient.shutdown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证OSS回调签名
|
|
|
+ *
|
|
|
+ * @param authorizationHeader Authorization头
|
|
|
+ * @param pubKeyUrl 公钥URL
|
|
|
+ * @param callbackBody 回调Body
|
|
|
+ * @return 是否验证通过
|
|
|
+ */
|
|
|
+ public boolean verifyCallback(String authorizationHeader, String pubKeyUrl, String callbackBody) {
|
|
|
+ try {
|
|
|
+ // 从公钥URL获取公钥
|
|
|
+ String pubKey = getPublicKey(pubKeyUrl);
|
|
|
+
|
|
|
+ // 验证签名
|
|
|
+ String[] authParts = authorizationHeader.split(":");
|
|
|
+ if (authParts.length != 2) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ String signature = authParts[1];
|
|
|
+ String stringToSign = callbackBody;
|
|
|
+
|
|
|
+ // 使用公钥验证签名
|
|
|
+ java.security.Signature signatureVerifier = java.security.Signature.getInstance("SHA1withRSA");
|
|
|
+ signatureVerifier.initVerify(java.security.KeyFactory.getInstance("RSA")
|
|
|
+ .generatePublic(new java.security.spec.X509EncodedKeySpec(
|
|
|
+ Base64.getDecoder().decode(pubKey))));
|
|
|
+ signatureVerifier.update(stringToSign.getBytes(StandardCharsets.UTF_8));
|
|
|
+
|
|
|
+ return signatureVerifier.verify(Base64.getDecoder().decode(signature));
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("验证OSS回调签名失败: {}", e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从URL获取公钥
|
|
|
+ */
|
|
|
+ private String getPublicKey(String pubKeyUrl) {
|
|
|
+ try {
|
|
|
+ java.net.URL url = new java.net.URL(pubKeyUrl);
|
|
|
+ java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
|
|
|
+ conn.setRequestMethod("GET");
|
|
|
+ conn.connect();
|
|
|
+
|
|
|
+ StringBuilder response = new StringBuilder();
|
|
|
+ try (java.io.BufferedReader reader = new java.io.BufferedReader(
|
|
|
+ new java.io.InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
|
|
+ String line;
|
|
|
+ while ((line = reader.readLine()) != null) {
|
|
|
+ response.append(line);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移除公钥的头部和尾部
|
|
|
+ String pubKey = response.toString();
|
|
|
+ pubKey = pubKey.replace("-----BEGIN PUBLIC KEY-----", "");
|
|
|
+ pubKey = pubKey.replace("-----END PUBLIC KEY-----", "");
|
|
|
+ pubKey = pubKey.replace("\n", "");
|
|
|
+ pubKey = pubKey.replace("\r", "");
|
|
|
+
|
|
|
+ return pubKey;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取公钥失败: {}", e.getMessage(), e);
|
|
|
+ throw new RuntimeException("获取公钥失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建OSS客户端
|
|
|
+ */
|
|
|
+ private OSS createOSSClient() {
|
|
|
+ com.aliyun.oss.ClientBuilderConfiguration config = new com.aliyun.oss.ClientBuilderConfiguration();
|
|
|
+ config.setSignatureVersion(SignVersion.V4);
|
|
|
+ return new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OSS签名结果
|
|
|
+ */
|
|
|
+ @Data
|
|
|
+ public static class OSSSignatureResult {
|
|
|
+ private String accessKeyId;
|
|
|
+ private String policy;
|
|
|
+ private String signature;
|
|
|
+ private String dir;
|
|
|
+ private String host;
|
|
|
+ private String expire;
|
|
|
+ private String ossKey;
|
|
|
+ private String callbackUrl;
|
|
|
+ private String callbackBody;
|
|
|
+ }
|
|
|
+}
|