From 7ca18b7798c6a4c2ff8e4b73a48cde6641a51ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=96=AF=E7=8B=82=E7=9A=84=E7=8B=AE=E5=AD=90Li?= <15040126243@163.com> Date: Wed, 1 Jul 2026 09:44:03 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E9=87=8D=E5=86=99=20s3=20=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E7=AD=BE=E5=90=8D=20=E6=94=AF=E6=8C=81=E7=A7=81?= =?UTF-8?q?=E6=9C=89=E5=BA=93=E8=87=AA=E5=AE=9A=E4=B9=89=E5=9F=9F=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oss/client/AbstractOssClientImpl.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/client/AbstractOssClientImpl.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/client/AbstractOssClientImpl.java index 7f16b58b1..5f44c8618 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/client/AbstractOssClientImpl.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/client/AbstractOssClientImpl.java @@ -15,6 +15,14 @@ import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.async.ResponsePublisher; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.auth.aws.signer.AwsV4FamilyHttpSigner; +import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner; +import software.amazon.awssdk.http.auth.spi.signer.HttpSigner; +import software.amazon.awssdk.http.auth.spi.signer.SignRequest; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; @@ -25,6 +33,7 @@ import software.amazon.awssdk.transfer.s3.S3TransferManager; import software.amazon.awssdk.transfer.s3.model.CompletedUpload; import software.amazon.awssdk.transfer.s3.model.DownloadRequest; import software.amazon.awssdk.transfer.s3.progress.TransferListener; +import software.amazon.awssdk.utils.http.SdkHttpUtils; import java.io.*; import java.nio.channels.Channels; @@ -33,8 +42,11 @@ import java.nio.channels.SeekableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Clock; import java.time.Duration; +import java.time.Instant; import java.time.ZoneOffset; +import java.net.URI; import java.util.*; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; @@ -716,6 +728,9 @@ public abstract class AbstractOssClientImpl implements OssClient { */ @Override public String bucketPresignGetUrl(String bucket, String key, Duration expiredTime) { + if (useBucketBoundDomain(bucket)) { + return bucketBoundDomainPresignUrl(SdkHttpMethod.GET, key, expiredTime, Collections.emptyMap()); + } try { return s3Presigner.presignGetObject(getObjectPresignRequestBuilder -> { getObjectPresignRequestBuilder.signatureDuration(expiredTime) @@ -739,6 +754,9 @@ public abstract class AbstractOssClientImpl implements OssClient { */ @Override public String bucketPresignPutUrl(String bucket, String key, Duration expiredTime, Map metadata) { + if (useBucketBoundDomain(bucket)) { + return bucketBoundDomainPresignUrl(SdkHttpMethod.PUT, key, expiredTime, metadata); + } try { return s3Presigner.presignPutObject(putObjectPresignRequestBuilder -> { putObjectPresignRequestBuilder.signatureDuration(expiredTime) @@ -751,6 +769,94 @@ public abstract class AbstractOssClientImpl implements OssClient { } } + /** + * 是否使用已绑定默认桶的自定义域名生成无桶名预签名 URL。 + * + * @param bucket 存储桶名称 + * @return 是否使用自定义域名签名 + */ + private boolean useBucketBoundDomain(String bucket) { + return config.domain() + .filter(StringUtils::isNotBlank) + .isPresent() && config.bucket() + .filter(defaultBucket -> Objects.equals(defaultBucket, bucket)) + .isPresent(); + } + + /** + * 使用绑定默认桶的自定义域名生成预签名 URL,避免 S3 SDK 自动拼接桶名。 + * + * @param method HTTP 方法 + * @param key 对象键 + * @param expiredTime 过期时间 + * @param metadata 对象元数据 + * @return 预签名 URL + */ + private String bucketBoundDomainPresignUrl(SdkHttpMethod method, String key, Duration expiredTime, Map metadata) { + try { + URI domainUri = URI.create(config.getDomainUrl()); + SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder() + .method(method) + .uri(domainUri) + .encodedPath(bucketBoundDomainPath(domainUri, key)); + if (metadata != null) { + metadata.forEach((metadataKey, metadataValue) -> { + if (StringUtils.isNotBlank(metadataKey)) { + requestBuilder.putHeader("x-amz-meta-" + metadataKey, String.valueOf(metadataValue)); + } + }); + } + AwsCredentialsIdentity credentials = AwsCredentialsIdentity.create( + config.accessKey() + .filter(StringUtils::isNotBlank) + .orElseThrow(() -> S3StorageException.form("accessKey is not configured.")), + config.secretKey() + .filter(StringUtils::isNotBlank) + .orElseThrow(() -> S3StorageException.form("secretKey is not configured.")) + ); + Clock signingClock = Clock.fixed(Instant.now(), ZoneOffset.UTC); + return AwsV4HttpSigner.create() + .sign(SignRequest.builder(credentials) + .request(requestBuilder.build()) + .putProperty(AwsV4HttpSigner.REGION_NAME, config.region().orElse(Region.US_EAST_1).id()) + .putProperty(AwsV4FamilyHttpSigner.SERVICE_SIGNING_NAME, "s3") + .putProperty(AwsV4FamilyHttpSigner.AUTH_LOCATION, AwsV4FamilyHttpSigner.AuthLocation.QUERY_STRING) + .putProperty(AwsV4FamilyHttpSigner.PAYLOAD_SIGNING_ENABLED, false) + .putProperty(AwsV4FamilyHttpSigner.EXPIRATION_DURATION, expiredTime) + .putProperty(HttpSigner.SIGNING_CLOCK, signingClock) + .putProperty(AwsV4FamilyHttpSigner.DOUBLE_URL_ENCODE, false) + .putProperty(AwsV4FamilyHttpSigner.NORMALIZE_PATH, false) + .build()) + .request() + .getUri() + .toString(); + } catch (Exception e) { + throw toStorageException(e); + } + } + + /** + * 构建绑定桶域名下的对象访问路径。 + * + * @param domainUri 自定义域名 URI + * @param key 对象键 + * @return 编码后的访问路径 + */ + private String bucketBoundDomainPath(URI domainUri, String key) { + String basePath = Optional.ofNullable(domainUri.getRawPath()) + .filter(StringUtils::isNotBlank) + .filter(path -> !"/".equals(path)) + .orElse(""); + String objectPath = SdkHttpUtils.urlEncodeIgnoreSlashes(key); + if (!basePath.startsWith("/")) { + basePath = "/" + basePath; + } + if (!basePath.endsWith("/")) { + basePath = basePath + "/"; + } + return basePath + objectPath; + } + /** * 上传本地路径文件到默认存储桶。 *