diff --git a/pom.xml b/pom.xml index 6441d6a7b..a7f6a9eb9 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 2.7.0 - 1.12.600 + 2.22.9 2.2.0 @@ -236,10 +236,13 @@ - com.amazonaws - aws-java-sdk-s3 + software.amazon.awssdk + bom ${aws-java-sdk-s3.version} + pom + import + org.dromara.sms4j diff --git a/ruoyi-common/ruoyi-common-oss/pom.xml b/ruoyi-common/ruoyi-common-oss/pom.xml index 8e7affff4..90cc13c9c 100644 --- a/ruoyi-common/ruoyi-common-oss/pom.xml +++ b/ruoyi-common/ruoyi-common-oss/pom.xml @@ -27,8 +27,36 @@ - com.amazonaws - aws-java-sdk-s3 + software.amazon.awssdk + s3 + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + + + + + software.amazon.awssdk + kms + + + software.amazon.awssdk + s3control + + + software.amazon.awssdk + s3-transfer-manager + + + software.amazon.awssdk.crt + aws-crt + 0.29.1 diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java index 53e05c927..62986049e 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java @@ -2,18 +2,6 @@ package org.dromara.common.oss.core; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.IdUtil; -import com.amazonaws.ClientConfiguration; -import com.amazonaws.HttpMethod; -import com.amazonaws.Protocol; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.*; import org.dromara.common.core.utils.DateUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.oss.constant.OssConstant; @@ -22,18 +10,29 @@ import org.dromara.common.oss.enumd.AccessPolicyType; import org.dromara.common.oss.enumd.PolicyType; import org.dromara.common.oss.exception.OssException; import org.dromara.common.oss.properties.OssProperties; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.*; +import software.amazon.awssdk.services.s3.model.*; + +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; +import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; import java.io.ByteArrayInputStream; import java.io.File; import java.io.InputStream; +import java.net.URI; import java.net.URL; -import java.util.Date; +import java.time.Duration; /** - * S3 存储协议 所有兼容S3协议的云厂商均支持 + * S3-v2 存储协议 所有兼容S3协议的云厂商均支持 * 阿里云 腾讯云 七牛云 minio * - * @author Lion Li + * @author Lion Li / David Wei */ public class OssClient { @@ -41,39 +40,40 @@ public class OssClient { private final OssProperties properties; - private final AmazonS3 client; + private final S3Client client; + + private final S3Presigner presigner; public OssClient(String configKey, OssProperties ossProperties) { this.configKey = configKey; this.properties = ossProperties; try { - AwsClientBuilder.EndpointConfiguration endpointConfig = - new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion()); + StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())); - AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey()); - AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials); - ClientConfiguration clientConfig = new ClientConfiguration(); - if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) { - clientConfig.setProtocol(Protocol.HTTPS); - } else { - clientConfig.setProtocol(Protocol.HTTP); - } - AmazonS3ClientBuilder build = AmazonS3Client.builder() - .withEndpointConfiguration(endpointConfig) - .withClientConfiguration(clientConfig) - .withCredentials(credentialsProvider) - .disableChunkedEncoding(); + S3Configuration.Builder config = S3Configuration.builder() + .chunkedEncodingEnabled(false); if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) { // minio 使用https限制使用域名访问 需要此配置 站点填域名 - build.enablePathStyleAccess(); + config.pathStyleAccessEnabled(true); } - this.client = build.build(); + S3ClientBuilder client = S3Client.builder() + .credentialsProvider(credentialsProvider) + .endpointOverride(URI.create(getEndpoint())) + .region(Region.of(properties.getRegion())) + .serviceConfiguration(config.build()); + + S3Presigner.Builder presigner = S3Presigner.builder() + .credentialsProvider(credentialsProvider) + .endpointOverride(URI.create(getPresignerEndpoint())) + .region(Region.of(properties.getRegion())) + .serviceConfiguration(config.build()); + + this.client = client.build(); + this.presigner = presigner.build(); createBucket(); } catch (Exception e) { - if (e instanceof OssException) { - throw e; - } throw new OssException("配置错误! 请检查系统配置:[" + e.getMessage() + "]"); } } @@ -81,14 +81,27 @@ public class OssClient { public void createBucket() { try { String bucketName = properties.getBucketName(); - if (client.doesBucketExistV2(bucketName)) { + try { + client.headBucket(HeadBucketRequest.builder() + .bucket(bucketName) + .build()); return; + } catch (Exception e) { + // 桶不存在,捕获异常。 + // 继续桶创建操作 } - CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName); AccessPolicyType accessPolicy = getAccessPolicy(); - createBucketRequest.setCannedAcl(accessPolicy.getAcl()); + CreateBucketRequest createBucketRequest = CreateBucketRequest.builder() + .bucket(bucketName) + .acl(accessPolicy.getBucketCannedACL()) + .build(); + + PutBucketPolicyRequest putBucketPolicyRequest = PutBucketPolicyRequest.builder() + .bucket(bucketName) + .policy(getPolicy(bucketName, accessPolicy.getPolicyType())) + .build(); client.createBucket(createBucketRequest); - client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType())); + client.putBucketPolicy(putBucketPolicyRequest); } catch (Exception e) { throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]"); } @@ -103,13 +116,13 @@ public class OssClient { inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream)); } try { - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentType(contentType); - metadata.setContentLength(inputStream.available()); - PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata); - // 设置上传对象的 Acl 为公共读 - putObjectRequest.setCannedAcl(getAccessPolicy().getAcl()); - client.putObject(putObjectRequest); + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(properties.getBucketName()) + .key(path) + .contentType(contentType) + .acl(getAccessPolicy().getObjectCannedACL()) // 设置上传对象的 Acl + .build(); + client.putObject(putObjectRequest, RequestBody.fromInputStream(inputStream, inputStream.available())); } catch (Exception e) { throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]"); } @@ -118,10 +131,12 @@ public class OssClient { public UploadResult upload(File file, String path) { try { - PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file); - // 设置上传对象的 Acl 为公共读 - putObjectRequest.setCannedAcl(getAccessPolicy().getAcl()); - client.putObject(putObjectRequest); + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(properties.getBucketName()) + .key(path) + .acl(getAccessPolicy().getObjectCannedACL())// 设置上传对象的 Acl + .build(); + client.putObject(putObjectRequest, RequestBody.fromFile(file)); } catch (Exception e) { throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]"); } @@ -131,7 +146,11 @@ public class OssClient { public void delete(String path) { path = path.replace(getUrl() + "/", ""); try { - client.deleteObject(properties.getBucketName(), path); + DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() + .bucket(properties.getBucketName()) + .key(path) + .build(); + client.deleteObject(deleteObjectRequest); } catch (Exception e) { throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]"); } @@ -149,21 +168,35 @@ public class OssClient { return upload(file, getPath(properties.getPrefix(), suffix)); } - /** - * 获取文件元数据 - * - * @param path 完整文件路径 - */ - public ObjectMetadata getObjectMetadata(String path) { - path = path.replace(getUrl() + "/", ""); - S3Object object = client.getObject(properties.getBucketName(), path); - return object.getObjectMetadata(); - } - public InputStream getObjectContent(String path) { path = path.replace(getUrl() + "/", ""); - S3Object object = client.getObject(properties.getBucketName(), path); - return object.getObjectContent(); + + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(properties.getBucketName()) + .key(path) + .build(); + return client.getObject(getObjectRequest); + } + + public String getEndpoint() { + String endpoint = properties.getEndpoint(); + String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://"; + return header + endpoint; + } + + /** + * 获取预签名地址的 Endpoint 地址 + * 注意:此地址不能带bucket,「云服务商」使用非路径形式,生成的url会自动带上bucket前缀 + * @return + */ + public String getPresignerEndpoint() { + String domain = properties.getDomain(); + String endpoint = properties.getEndpoint(); + String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://"; + if (StringUtils.isNotBlank(domain)) { + return header + domain; + } + return header + endpoint; } public String getUrl() { @@ -207,12 +240,24 @@ public class OssClient { * @param second 授权时间 */ public String getPrivateUrl(String objectKey, Integer second) { - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey) - .withMethod(HttpMethod.GET) - .withExpiration(new Date(System.currentTimeMillis() + 1000L * second)); - URL url = client.generatePresignedUrl(generatePresignedUrlRequest); - return url.toString(); + try { + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(properties.getBucketName()) + .key(objectKey) + .build(); + GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder() + .signatureDuration(Duration.ofSeconds(second)) + .getObjectRequest(getObjectRequest) + .build(); + + PresignedGetObjectRequest presignedGetObjectRequest = presigner + .presignGetObject(getObjectPresignRequest); + URL url = presignedGetObjectRequest.url(); + + return url.toString(); + } catch (Exception e) { + return null; + } } /** @@ -231,32 +276,108 @@ public class OssClient { return AccessPolicyType.getByType(properties.getAccessPolicy()); } + /** + * 获取 minio 的 policy 定义 + * @param bucketName + * @param policyType + * @return + */ private static String getPolicy(String bucketName, PolicyType policyType) { - StringBuilder builder = new StringBuilder(); - builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n"); - builder.append(switch (policyType) { - case WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n"; - case READ_WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n"; - default -> "\"s3:GetBucketLocation\"\n"; - }); - builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); - builder.append(bucketName); - builder.append("\"\n},\n"); - if (policyType == PolicyType.READ) { - builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); - builder.append(bucketName); - builder.append("\"\n},\n"); - } - builder.append("{\n\"Action\": "); - builder.append(switch (policyType) { - case WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n"; - case READ_WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n"; - default -> "\"s3:GetObject\",\n"; - }); - builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); - builder.append(bucketName); - builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n"); - return builder.toString(); - } + String policy = switch (policyType) { + case WRITE -> """ + { + "Version": "2012-10-17", + "Statement": [] + } + """; + case READ_WRITE -> """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": [ + "*" + ] + }, + "Action": [ + "s3:GetBucketLocation", + "s3:ListBucket", + "s3:ListBucketMultipartUploads" + ], + "Resource": [ + "arn:aws:s3:::bucketName" + ] + }, + { + "Effect": "Allow", + "Principal": { + "AWS": [ + "*" + ] + }, + "Action": [ + "s3:AbortMultipartUpload", + "s3:DeleteObject", + "s3:GetObject", + "s3:ListMultipartUploadParts", + "s3:PutObject" + ], + "Resource": [ + "arn:aws:s3:::bucketName/*" + ] + } + ] + } + """; + case READ -> """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": [ + "*" + ] + }, + "Action": [ + "s3:GetBucketLocation" + ], + "Resource": [ + "arn:aws:s3:::bucketName" + ] + }, + { + "Effect": "Deny", + "Principal": "*", + "Action": [ + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::bucketName" + ] + }, + { + "Effect": "Allow", + "Principal": { + "AWS": [ + "*" + ] + }, + "Action": [ + "s3:GetObject" + ], + "Resource": [ + "arn:aws:s3:::bucketName/*" + ] + } + ] + } + """; + }; + return policy.replaceAll("bucketName", bucketName); + } } diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java index 9074d72de..6d39133d5 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java @@ -1,8 +1,9 @@ package org.dromara.common.oss.enumd; -import com.amazonaws.services.s3.model.CannedAccessControlList; import lombok.AllArgsConstructor; import lombok.Getter; +import software.amazon.awssdk.services.s3.model.BucketCannedACL; +import software.amazon.awssdk.services.s3.model.ObjectCannedACL; /** * 桶访问策略配置 @@ -16,27 +17,32 @@ public enum AccessPolicyType { /** * private */ - PRIVATE("0", CannedAccessControlList.Private, PolicyType.WRITE), + PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE, PolicyType.WRITE), /** * public */ - PUBLIC("1", CannedAccessControlList.PublicRead, PolicyType.READ), + PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE, PolicyType.READ_WRITE), /** * custom */ - CUSTOM("2",CannedAccessControlList.PublicRead, PolicyType.READ); + CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ, PolicyType.READ); /** - * 桶 权限类型 + * 桶 权限类型(数据库值) */ private final String type; + /** + * 桶 权限类型 + */ + private final BucketCannedACL bucketCannedACL; + /** * 文件对象 权限类型 */ - private final CannedAccessControlList acl; + private final ObjectCannedACL objectCannedACL; /** * 桶策略类型