🐛 修复 MinIO endpoint 含 path 导致 blade-transport 启动失败

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-13 22:14:50 +08:00
parent a7bf2588b6
commit 7c7fe2e1c1
3 changed files with 109 additions and 7 deletions
@@ -5,19 +5,28 @@
package org.springblade.transport.config;
import io.minio.MinioClient;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.URI;
/**
* 凭证图片 MinIO 客户端配置。
* <p>
* 连接参数由 Nacos 的 file.storage.minio 配置提供。
* <ul>
* <li>{@code endpoint}:内网地址,供服务端 put/get,不能带 path</li>
* <li>{@code transform-endpoint}:外网地址(可带 /oss),仅用于浏览器预签名</li>
* </ul>
* 未配置 {@code file.storage.minio.endpoint} 时不注册该客户端,
* 以免凭证上传功能缺失配置导致整个 blade-transport 服务无法启动;
* 此时凭证相关接口会在调用时给出明确提示,而非启动即失败。
* 以免凭证上传功能缺失配置导致整个 blade-transport 服务无法启动。
*/
@Slf4j
@Configuration
@ConditionalOnProperty(prefix = "file.storage.minio", name = "endpoint")
public class VoucherMinioConfig {
@@ -27,6 +36,30 @@ public class VoucherMinioConfig {
@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) {
return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();
String clientEndpoint = resolveClientEndpoint(endpoint);
if (!clientEndpoint.equals(endpoint == null ? null : endpoint.trim())) {
log.warn("MinIO endpoint 含 path,已剥离为 {}(原值={})。服务端读写请配置内网 endpoint,外网请用 transform-endpoint",
clientEndpoint, endpoint);
}
return MinioClient.builder().endpoint(clientEndpoint).credentials(accessKey, secretKey).build();
}
/**
* MinIO SDK 不允许 endpoint 带 path(例如 https://host/oss)。
*/
public static String resolveClientEndpoint(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 origin.toString();
}
}
@@ -88,6 +88,10 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
private final ObjectProvider<MinioClient> minioClientProvider;
private final String minioBucketName;
private final String minioRootDirectory;
private final String minioTransformEndpoint;
private final String minioAccessKey;
private final String minioSecretKey;
private volatile MinioClient transformPresignClient;
public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService,
IWaybillService waybillService,
@@ -95,7 +99,10 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
LoadingManageMapper loadingManageMapper,
ApplicationEventPublisher eventPublisher, ObjectProvider<MinioClient> minioClientProvider,
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName,
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) {
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory,
@Value("${file.storage.minio.transform-endpoint:${minio.transform-endpoint:}}") String minioTransformEndpoint,
@Value("${file.storage.minio.access-key-id:${minio.access-key:}}") String minioAccessKey,
@Value("${file.storage.minio.access-key-secret:${minio.secret-key:}}") String minioSecretKey) {
this.voucherWaybillBatchMapper = voucherWaybillBatchMapper;
this.projectApplyService = projectApplyService;
this.waybillService = waybillService;
@@ -106,6 +113,9 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
this.minioClientProvider = minioClientProvider;
this.minioBucketName = minioBucketName;
this.minioRootDirectory = minioRootDirectory;
this.minioTransformEndpoint = minioTransformEndpoint;
this.minioAccessKey = minioAccessKey;
this.minioSecretKey = minioSecretKey;
}
@Override
@@ -1490,14 +1500,71 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
private String buildVoucherFileUrl(Long fileId, String objectKey) {
try {
return minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
String presignedUrl = resolvePresignClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET).bucket(minioBucketName).object(objectKey).expiry(1, java.util.concurrent.TimeUnit.HOURS).build());
return applyTransformPathPrefix(presignedUrl);
} catch (Exception exception) {
log.warn("生成凭证文件预览地址失败 voucherFileId={}, objectKey={}", fileId, objectKey, exception);
return null;
}
}
/**
* 浏览器预签名优先使用外网地址,避免 HTTPS 页面访问 HTTP MinIO 被 Mixed Content 拦截。
*/
private MinioClient resolvePresignClient() {
if (Func.isEmpty(minioTransformEndpoint)) {
return minioClient();
}
MinioClient cachedClient = transformPresignClient;
if (cachedClient != null) {
return cachedClient;
}
synchronized (this) {
if (transformPresignClient == null) {
transformPresignClient = MinioClient.builder()
.endpoint(org.springblade.transport.config.VoucherMinioConfig.resolveClientEndpoint(minioTransformEndpoint))
.credentials(minioAccessKey, minioSecretKey)
.build();
}
return transformPresignClient;
}
}
private String applyTransformPathPrefix(String presignedUrl) {
if (Func.isEmpty(minioTransformEndpoint) || Func.isEmpty(presignedUrl)) {
return presignedUrl;
}
java.net.URI transformUri = java.net.URI.create(minioTransformEndpoint.trim());
String pathPrefix = normalizePathPrefix(transformUri.getPath());
if (Func.isEmpty(pathPrefix)) {
return presignedUrl;
}
java.net.URI url = java.net.URI.create(presignedUrl);
String path = url.getRawPath() == null ? "" : url.getRawPath();
if (path.equals(pathPrefix) || path.startsWith(pathPrefix + "/")) {
return presignedUrl;
}
StringBuilder result = new StringBuilder();
result.append(url.getScheme()).append("://").append(url.getHost());
if (url.getPort() > 0) {
result.append(':').append(url.getPort());
}
result.append(pathPrefix).append(path);
if (Func.isNotEmpty(url.getRawQuery())) {
result.append('?').append(url.getRawQuery());
}
return result.toString();
}
private String normalizePathPrefix(String path) {
if (Func.isEmpty(path) || "/".equals(path)) {
return "";
}
String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
return normalized.startsWith("/") ? normalized : "/" + normalized;
}
private String normalizePlateNo(String plateNo) {
return plateNo == null ? null : plateNo.replaceAll("[\\s-]", "").toUpperCase();
}
+4 -2
View File
@@ -120,11 +120,13 @@ baidu:
request-timeout: 30s
token-refresh-advance: 1m
# 凭证分片上传走 blade-file,与 /resource/oss 库表配置相互独立;外网地址必须在此配置并发布到 Nacos 后重启 blade-file
# 不要使用可被空环境变量覆盖的 ${VAR:default},否则仍会签发 http://内网IP 预签名导致 Mixed Content
# 对象存储(blade-file 分片上传 + blade-transport 凭证读写共用)
# endpoint:内网地址,禁止带 path,供服务端 put/get(如 http://tms-prod-oss:9000 或 http://IP:19000)
# transform-endpoint:外网 HTTPS,可带 /oss,仅用于浏览器预签名;切勿把该值填到 endpoint
file:
storage:
minio:
# endpoint: http://内网MinIO地址:端口
transform-endpoint: https://tms-prod.xinghy56.com/oss
powerjob: