diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java b/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java
index 3e578fef..07748388 100644
--- a/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java
@@ -11,6 +11,7 @@ import org.springblade.file.service.impl.LocalFileServiceImpl;
import org.springblade.file.service.impl.MinioFileServiceImpl;
import org.springblade.file.service.impl.OssFileServiceImpl;
import org.springblade.file.service.impl.ObsFileServiceImpl;
+import org.springblade.file.util.MinioClientFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -65,19 +66,22 @@ public class FileConfig {
private IFileService buildMinioFileService(FileStorageProperties.StoreProperties properties) {
validateStoreProperties(STORAGE_MINIO, properties);
+ String endpointOrigin = MinioClientFactory.resolveOrigin(properties.getEndpoint());
+ String endpointPathPrefix = MinioClientFactory.resolvePathPrefix(properties.getEndpoint());
+ if (StringUtil.isNotBlank(endpointPathPrefix)) {
+ log.warn("MinIO endpoint 含 path,将按 origin={} pathPrefix={} 访问;建议 endpoint 使用内网地址,transform-endpoint 使用外网 HTTPS",
+ endpointOrigin, endpointPathPrefix);
+ }
if (StringUtil.isBlank(properties.getTransformEndpoint())
- && StringUtil.isNotBlank(properties.getEndpoint())
&& properties.getEndpoint().trim().toLowerCase().startsWith("http://")) {
log.warn("MinIO transform-endpoint 未配置,endpoint={}。HTTPS 页面分片直传会被浏览器 Mixed Content 拦截,将回退为服务端代传",
properties.getEndpoint());
} else {
- log.info("MinIO 文件存储已启用 endpoint={} transformEndpoint={}",
- properties.getEndpoint(), properties.getTransformEndpoint());
+ log.info("MinIO 文件存储已启用 endpoint={} pathPrefix={} transformEndpoint={}",
+ endpointOrigin, endpointPathPrefix, properties.getTransformEndpoint());
}
- MinioClient minioClient = MinioClient.builder()
- .endpoint(properties.getEndpoint())
- .credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
- .build();
+ MinioClient minioClient = MinioClientFactory.create(
+ properties.getEndpoint(), properties.getAccessKeyId(), properties.getAccessKeySecret());
return new MinioFileServiceImpl(properties, minioClient);
}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/MinioFileServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/MinioFileServiceImpl.java
index 45741733..c7d2ed4e 100644
--- a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/MinioFileServiceImpl.java
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/MinioFileServiceImpl.java
@@ -40,6 +40,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.file.config.FileStorageProperties;
+import org.springblade.file.util.MinioClientFactory;
import org.springblade.file.pojo.vo.PartEtagVO;
import java.io.File;
@@ -65,8 +66,7 @@ import java.util.UUID;
public class MinioFileServiceImpl extends AbstractFileService {
private static final long PART_SIZE = 10 * 1024 * 1024L;
- /** MinIO 默认 region;固定后预签名无需再请求外网域名做 GetBucketLocation */
- private static final String DEFAULT_REGION = "us-east-1";
+ private static final String DEFAULT_REGION = MinioClientFactory.DEFAULT_REGION;
private final FileStorageProperties.StoreProperties properties;
private final MinioClient minioClient;
@@ -216,10 +216,7 @@ public class MinioFileServiceImpl extends AbstractFileService {
/**
* 浏览器侧预签名优先使用外网地址,避免 HTTPS 页面访问 HTTP MinIO 被 Mixed Content 拦截。
- *
* MinIO SDK 不允许 endpoint 带 path,因此外网地址中的 /oss 等前缀在签名后单独拼回。
- * 外网域名通常是业务网关而非直连 MinIO,必须固定 region,否则 SDK 会请求 GetBucketLocation
- * 并拿到 HTML 页面导致 XmlParserException。
*/
private MinioClient resolvePresignClient() {
String transformEndpoint = properties.getTransformEndpoint();
@@ -232,29 +229,13 @@ public class MinioFileServiceImpl extends AbstractFileService {
}
synchronized (this) {
if (transformPresignClient == null) {
- transformPresignClient = MinioClient.builder()
- .endpoint(resolveTransformOrigin(transformEndpoint))
- .credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
- .region(DEFAULT_REGION)
- .build();
+ transformPresignClient = MinioClientFactory.create(
+ transformEndpoint, properties.getAccessKeyId(), properties.getAccessKeySecret());
}
return transformPresignClient;
}
}
- private String resolveTransformOrigin(String transformEndpoint) {
- URI transformUri = URI.create(transformEndpoint.trim());
- if (StringUtil.isBlank(transformUri.getScheme()) || StringUtil.isBlank(transformUri.getHost())) {
- throw new ServiceException("MinIO外网地址配置不正确");
- }
- StringBuilder origin = new StringBuilder();
- origin.append(transformUri.getScheme()).append("://").append(transformUri.getHost());
- if (transformUri.getPort() > 0) {
- origin.append(':').append(transformUri.getPort());
- }
- return origin.toString();
- }
-
private String applyTransformPathPrefix(String presignedUrl) {
String transformEndpoint = properties.getTransformEndpoint();
if (StringUtil.isBlank(transformEndpoint) || StringUtil.isBlank(presignedUrl)) {
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/util/MinioClientFactory.java b/blade-service/blade-file/src/main/java/org/springblade/file/util/MinioClientFactory.java
new file mode 100644
index 00000000..384dc61e
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/util/MinioClientFactory.java
@@ -0,0 +1,121 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.file.util;
+
+import io.minio.MinioClient;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.tool.utils.StringUtil;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * MinIO 客户端构建。
+ *
+ * SDK 不允许 endpoint 带 path;若配置为 {@code https://host/oss},则:
+ *
+ * - endpoint 使用 origin
+ * - 通过 OkHttp 拦截器把 /oss 拼回每个 API 请求
+ * - 固定 region,避免 GetBucketLocation 打到业务网关拿到 HTML
+ *
+ */
+public final class MinioClientFactory {
+
+ public static final String DEFAULT_REGION = "us-east-1";
+
+ private MinioClientFactory() {
+ }
+
+ public static MinioClient create(String endpoint, String accessKey, String secretKey) {
+ EndpointParts parts = parseEndpoint(endpoint);
+ MinioClient.Builder builder = MinioClient.builder()
+ .endpoint(parts.origin())
+ .credentials(accessKey, secretKey)
+ .region(DEFAULT_REGION);
+ if (StringUtil.isNotBlank(parts.pathPrefix())) {
+ builder.httpClient(buildHttpClient(parts.pathPrefix()), true);
+ }
+ return builder.build();
+ }
+
+ public static String resolveOrigin(String endpoint) {
+ return parseEndpoint(endpoint).origin();
+ }
+
+ public static String resolvePathPrefix(String endpoint) {
+ return parseEndpoint(endpoint).pathPrefix();
+ }
+
+ private static EndpointParts parseEndpoint(String endpoint) {
+ if (StringUtil.isBlank(endpoint)) {
+ throw new ServiceException("MinIO endpoint 不能为空");
+ }
+ URI uri = URI.create(endpoint.trim());
+ if (StringUtil.isBlank(uri.getScheme()) || StringUtil.isBlank(uri.getHost())) {
+ throw new ServiceException("MinIO endpoint 配置不正确: " + endpoint);
+ }
+ StringBuilder origin = new StringBuilder();
+ origin.append(uri.getScheme()).append("://").append(uri.getHost());
+ if (uri.getPort() > 0) {
+ origin.append(':').append(uri.getPort());
+ }
+ return new EndpointParts(origin.toString(), normalizePathPrefix(uri.getPath()));
+ }
+
+ private static String normalizePathPrefix(String path) {
+ if (StringUtil.isBlank(path) || "/".equals(path)) {
+ return "";
+ }
+ String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
+ return normalized.startsWith("/") ? normalized : "/" + normalized;
+ }
+
+ private static OkHttpClient buildHttpClient(String pathPrefix) {
+ return new OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .addInterceptor(chain -> {
+ Request original = chain.request();
+ HttpUrl originalUrl = original.url();
+ String encodedPath = originalUrl.encodedPath();
+ if (encodedPath.startsWith(pathPrefix + "/") || encodedPath.equals(pathPrefix)) {
+ return chain.proceed(original);
+ }
+ HttpUrl newUrl = originalUrl.newBuilder()
+ .encodedPath(pathPrefix + encodedPath)
+ .build();
+ return chain.proceed(original.newBuilder().url(newUrl).build());
+ })
+ .build();
+ }
+
+ private record EndpointParts(String origin, String pathPrefix) {
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherMinioConfig.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherMinioConfig.java
index 5c0db187..269eb1a5 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherMinioConfig.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherMinioConfig.java
@@ -6,6 +6,9 @@ package org.springblade.transport.config;
import io.minio.MinioClient;
import lombok.extern.slf4j.Slf4j;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.factory.annotation.Value;
@@ -14,34 +17,49 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.URI;
+import java.util.concurrent.TimeUnit;
/**
* 凭证图片 MinIO 客户端配置。
*
* 连接参数由 Nacos 的 file.storage.minio 配置提供。
*
- * - {@code endpoint}:内网地址,供服务端 put/get,不能带 path
- * - {@code transform-endpoint}:外网地址(可带 /oss),仅用于浏览器预签名
+ * - {@code endpoint}:建议内网地址;若误配为 https://host/oss,会自动剥离 path 并用拦截器补回
+ * - {@code transform-endpoint}:外网地址(可带 /oss),用于浏览器预签名
*
- * 未配置 {@code file.storage.minio.endpoint} 时不注册该客户端,
- * 以免凭证上传功能缺失配置导致整个 blade-transport 服务无法启动。
*/
@Slf4j
@Configuration
@ConditionalOnProperty(prefix = "file.storage.minio", name = "endpoint")
public class VoucherMinioConfig {
+ private static final String DEFAULT_REGION = "us-east-1";
+
@Bean
public MinioClient voucherMinioClient(
@Value("${file.storage.minio.endpoint:${minio.endpoint:}}") String endpoint,
@Value("${file.storage.minio.access-key-id:${minio.access-key:}}") String accessKey,
@Value("${file.storage.minio.access-key-secret:${minio.secret-key:}}") String secretKey) {
- String clientEndpoint = resolveClientEndpoint(endpoint);
- if (!clientEndpoint.equals(endpoint == null ? null : endpoint.trim())) {
- log.warn("MinIO endpoint 含 path,已剥离为 {}(原值={})。服务端读写请配置内网 endpoint,外网请用 transform-endpoint",
- clientEndpoint, endpoint);
+ String origin = resolveClientEndpoint(endpoint);
+ String pathPrefix = resolvePathPrefix(endpoint);
+ if (StringUtil.isNotBlank(pathPrefix)) {
+ log.warn("MinIO endpoint 含 path,将按 origin={} pathPrefix={} 访问;建议 endpoint 使用内网地址",
+ origin, pathPrefix);
}
- return MinioClient.builder().endpoint(clientEndpoint).credentials(accessKey, secretKey).build();
+ return create(endpoint, accessKey, secretKey);
+ }
+
+ public static MinioClient create(String endpoint, String accessKey, String secretKey) {
+ String origin = resolveClientEndpoint(endpoint);
+ String pathPrefix = resolvePathPrefix(endpoint);
+ MinioClient.Builder builder = MinioClient.builder()
+ .endpoint(origin)
+ .credentials(accessKey, secretKey)
+ .region(DEFAULT_REGION);
+ if (StringUtil.isNotBlank(pathPrefix)) {
+ builder.httpClient(buildHttpClient(pathPrefix), true);
+ }
+ return builder.build();
}
/**
@@ -62,4 +80,34 @@ public class VoucherMinioConfig {
}
return origin.toString();
}
+
+ private static String resolvePathPrefix(String endpoint) {
+ URI uri = URI.create(endpoint.trim());
+ String path = uri.getPath();
+ if (StringUtil.isBlank(path) || "/".equals(path)) {
+ return "";
+ }
+ String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
+ return normalized.startsWith("/") ? normalized : "/" + normalized;
+ }
+
+ private static OkHttpClient buildHttpClient(String pathPrefix) {
+ return new OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .addInterceptor(chain -> {
+ Request original = chain.request();
+ HttpUrl originalUrl = original.url();
+ String encodedPath = originalUrl.encodedPath();
+ if (encodedPath.startsWith(pathPrefix + "/") || encodedPath.equals(pathPrefix)) {
+ return chain.proceed(original);
+ }
+ HttpUrl newUrl = originalUrl.newBuilder()
+ .encodedPath(pathPrefix + encodedPath)
+ .build();
+ return chain.proceed(original.newBuilder().url(newUrl).build());
+ })
+ .build();
+ }
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java
index 6f6b7d4e..e01c3fc7 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java
@@ -1525,11 +1525,8 @@ public class VoucherManageServiceImpl extends BaseServiceImpl