🐛 修复 MinIO 预签名走外网 HTTPS,并过滤无效应收应付变更记录

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-13 12:44:39 +08:00
parent ea1829cd75
commit e0da79d745
5 changed files with 166 additions and 97 deletions
@@ -95,7 +95,13 @@ public class FileConfig {
FileStorageProperties.StoreProperties mergedProperties = new FileStorageProperties.StoreProperties();
String fileStoragePrefix = "file.storage." + prefix;
mergedProperties.setEndpoint(firstNotBlank(properties.getEndpoint(), environment.getProperty(fileStoragePrefix + ".endpoint"), environment.getProperty(prefix + ".endpoint")));
mergedProperties.setTransformEndpoint(firstNotBlank(properties.getTransformEndpoint(), environment.getProperty(fileStoragePrefix + ".transform-endpoint"), environment.getProperty(prefix + ".transform-endpoint")));
mergedProperties.setTransformEndpoint(firstNotBlank(
properties.getTransformEndpoint(),
environment.getProperty(fileStoragePrefix + ".transform-endpoint"),
environment.getProperty(prefix + ".transform-endpoint"),
// 兼容 blade-resource / oss 配置,避免仅改对象存储外网地址时分片上传仍走内网 HTTP
environment.getProperty("oss.transform-endpoint")
));
mergedProperties.setAccessKeyId(firstNotBlank(properties.getAccessKeyId(), environment.getProperty(fileStoragePrefix + ".access-key-id"), environment.getProperty(fileStoragePrefix + ".access-key"), environment.getProperty(prefix + ".access-key-id"), environment.getProperty(prefix + ".access-key")));
mergedProperties.setAccessKeySecret(firstNotBlank(properties.getAccessKeySecret(), environment.getProperty(fileStoragePrefix + ".access-key-secret"), environment.getProperty(fileStoragePrefix + ".secret-key"), environment.getProperty(prefix + ".access-key-secret"), environment.getProperty(prefix + ".secret-key")));
mergedProperties.setBucketName(firstNotBlank(properties.getBucketName(), environment.getProperty(fileStoragePrefix + ".bucket-name"), environment.getProperty(prefix + ".bucket-name")));
@@ -25,11 +25,11 @@
*/
package org.springblade.file.service.impl;
import io.minio.ComposeObjectArgs;
import io.minio.ComposeSource;
import io.minio.GetObjectArgs;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.ComposeObjectArgs;
import io.minio.ComposeSource;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.UploadObjectArgs;
@@ -42,6 +42,7 @@ import org.springblade.file.config.FileStorageProperties;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
@@ -64,6 +65,7 @@ public class MinioFileServiceImpl extends AbstractFileService {
private final FileStorageProperties.StoreProperties properties;
private final MinioClient minioClient;
private volatile MinioClient transformPresignClient;
@Override
public String initiateMultipartUpload(String objectKey) {
@@ -73,9 +75,13 @@ public class MinioFileServiceImpl extends AbstractFileService {
@Override
public String getUploadPartUrl(String objectKey, String uploadId, Integer partNumber, Long expires) {
try {
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.PUT).bucket(properties.getBucketName()).object(partObjectKey(objectKey, uploadId, partNumber))
.expiry(Math.toIntExact(resolveExpires(expires))).build());
String presignedUrl = resolvePresignClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(properties.getBucketName())
.object(partObjectKey(objectKey, uploadId, partNumber))
.expiry(Math.toIntExact(resolveExpires(expires)))
.build());
return applyTransformPathPrefix(presignedUrl);
} catch (Exception exception) {
log.error("MinIO获取分片上传链接异常 objectKey:{}", objectKey, exception);
throw new ServiceException("获取分片上传链接异常");
@@ -143,12 +149,13 @@ public class MinioFileServiceImpl extends AbstractFileService {
return null;
}
try {
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
String presignedUrl = resolvePresignClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(properties.getBucketName())
.object(resolveObjectKey(objectKey))
.expiry(Math.toIntExact(resolveExpires(expires)))
.build());
return applyTransformPathPrefix(presignedUrl);
} catch (Exception exception) {
log.error("MinIO获取文件链接异常 objectKey:{}", objectKey, exception);
throw new ServiceException("MinIO获取文件链接异常");
@@ -170,6 +177,77 @@ public class MinioFileServiceImpl extends AbstractFileService {
}
}
/**
* 浏览器侧预签名优先使用外网地址,避免 HTTPS 页面访问 HTTP MinIO 被 Mixed Content 拦截。
* MinIO SDK 不允许 endpoint 带 path,因此外网地址中的 /oss 等前缀在签名后单独拼回。
*/
private MinioClient resolvePresignClient() {
String transformEndpoint = properties.getTransformEndpoint();
if (StringUtil.isBlank(transformEndpoint)) {
return minioClient;
}
MinioClient cachedClient = transformPresignClient;
if (cachedClient != null) {
return cachedClient;
}
synchronized (this) {
if (transformPresignClient == null) {
transformPresignClient = MinioClient.builder()
.endpoint(resolveTransformOrigin(transformEndpoint))
.credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
.build();
}
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)) {
return presignedUrl;
}
String pathPrefix = normalizePathPrefix(URI.create(transformEndpoint.trim()).getPath());
if (StringUtil.isBlank(pathPrefix)) {
return presignedUrl;
}
URI url = 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 (StringUtil.isNotBlank(url.getRawQuery())) {
result.append('?').append(url.getRawQuery());
}
return result.toString();
}
private 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 String resolveObjectKey(String objectKey) {
if (StringUtil.isBlank(properties.getRootDirectory())) {
return objectKey;