🐛 修复凭证分片上传 Mixed Content,支持服务端代传兜底

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-13 20:44:09 +08:00
parent 44b4ae2d74
commit f606850814
6 changed files with 112 additions and 5 deletions
@@ -3,6 +3,7 @@ package org.springblade.file.config;
import com.aliyun.oss.OSSClientBuilder;
import com.obs.services.ObsClient;
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.springblade.file.service.IFileService;
@@ -20,6 +21,7 @@ import org.springframework.core.env.Environment;
* @author bfhuange
* @date 2024/10/21
*/
@Slf4j
@Configuration
public class FileConfig {
@@ -63,6 +65,15 @@ public class FileConfig {
private IFileService buildMinioFileService(FileStorageProperties.StoreProperties properties) {
validateStoreProperties(STORAGE_MINIO, properties);
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());
}
MinioClient minioClient = MinioClient.builder()
.endpoint(properties.getEndpoint())
.credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
@@ -95,12 +106,15 @@ 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")));
// 资源管理页 /resource/oss 的外网地址写在库表,不进 Spring Environment;凭证分片必须单独配 file.storage.minio.transform-endpoint
mergedProperties.setTransformEndpoint(firstNotBlank(
properties.getTransformEndpoint(),
environment.getProperty(fileStoragePrefix + ".transform-endpoint"),
environment.getProperty(fileStoragePrefix + ".transformEndpoint"),
environment.getProperty(prefix + ".transform-endpoint"),
// 兼容 blade-resource / oss 配置,避免仅改对象存储外网地址时分片上传仍走内网 HTTP
environment.getProperty("oss.transform-endpoint")
environment.getProperty(prefix + ".transformEndpoint"),
environment.getProperty("oss.transform-endpoint"),
environment.getProperty("oss.transformEndpoint")
));
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")));
@@ -113,7 +127,7 @@ public class FileConfig {
private String firstNotBlank(String... values) {
for (String value : values) {
if (StringUtil.isNotBlank(value)) {
return value;
return value.trim();
}
}
return null;
@@ -111,4 +111,26 @@ public class FileTaskController extends BladeController {
String uploadUrl = fileTaskService.getUploadUrl(id, partNumber);
return R.data(uploadUrl);
}
@ApiLog("上传任务-服务端代传分片")
@PostMapping(value = "/uploadPart", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
@Parameters({
@Parameter(name = "id", description = "文件任务id", in = ParameterIn.QUERY, schema = @Schema(type = "string")),
@Parameter(name = "partNumber", description = "分段编号", in = ParameterIn.QUERY, schema = @Schema(type = "int"))
})
@Operation(summary = "服务端代传分片", description = "HTTPS 页面无法直连 HTTP MinIO 时,由服务端转发分片到对象存储")
public R<String> uploadPart(Long id, Integer partNumber, org.springframework.web.multipart.MultipartFile file) throws java.io.IOException {
if (file == null || file.isEmpty()) {
return R.fail("分片内容不能为空");
}
java.io.File tempFile = java.io.File.createTempFile("file-part-", ".bin");
try {
file.transferTo(tempFile);
return R.data(fileTaskService.uploadPart(id, partNumber, tempFile));
} finally {
if (tempFile.exists() && !tempFile.delete()) {
tempFile.deleteOnExit();
}
}
}
}
@@ -74,6 +74,16 @@ public interface IFileTaskService extends IService<FileTask> {
*/
String getUploadUrl(Long id, Integer partNumber);
/**
* 服务端代传分片(HTTPS 页面无法直连 HTTP MinIO 时使用)
*
* @param id 文件任务id
* @param partNumber 分段编号
* @param partFile 分片临时文件
* @return ETag
*/
String uploadPart(Long id, Integer partNumber, java.io.File partFile);
/**
* 取消文件任务
* @param taskIds
@@ -259,6 +259,33 @@ public class FileTaskServiceImpl extends ServiceImpl<FileTaskMapper, FileTask> i
return fileService.getUploadPartUrl(fileTask.getObjectKey(), fileTask.getUploadId(), partNumber, null);
}
@Override
public String uploadPart(Long id, Integer partNumber, java.io.File partFile) {
FileTask fileTask = this.getById(id);
Func.requireNotNull(fileTask, "文件任务不存在");
if (FileTaskStatus.COMPLETED.getCode().equals(fileTask.getStatus())) {
throw new ServiceException("文件任务已完成");
}
if (FileTaskStatus.PAUSED.getCode().equals(fileTask.getStatus())) {
throw new ServiceException("文件任务已暂停");
}
if (partFile == null || !partFile.exists()) {
throw new ServiceException("分片文件不存在");
}
org.springblade.file.pojo.vo.PartEtagVO partEtag = fileService.uploadPart(
fileTask.getObjectKey(),
fileTask.getUploadId(),
partFile,
partNumber,
0L,
partFile.length()
);
if (partEtag == null || StringUtil.isBlank(partEtag.getEtag())) {
throw new ServiceException("分片上传未返回 ETag");
}
return partEtag.getEtag().replace("\"", "");
}
@Transactional(rollbackFor = Exception.class)
@Override
public void cancelBatchByIds(List<Long> taskIds) {
@@ -30,6 +30,7 @@ import io.minio.ComposeSource;
import io.minio.GetObjectArgs;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.ObjectWriteResponse;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.UploadObjectArgs;
@@ -39,8 +40,10 @@ 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.pojo.vo.PartEtagVO;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
@@ -88,6 +91,36 @@ public class MinioFileServiceImpl extends AbstractFileService {
}
}
@Override
public PartEtagVO uploadPart(String objectKey, String uploadId, File file, int partNumber, Long offset, Long partSize) {
long start = offset == null ? 0L : offset;
long size = partSize == null ? Math.max(0L, file.length() - start) : partSize;
try (FileInputStream inputStream = new FileInputStream(file)) {
long skipped = inputStream.skip(start);
if (skipped < start) {
throw new ServiceException("MinIO分段上传读取文件偏移失败");
}
ObjectWriteResponse response = minioClient.putObject(PutObjectArgs.builder()
.bucket(properties.getBucketName())
.object(partObjectKey(objectKey, uploadId, partNumber))
.stream(inputStream, size, PART_SIZE)
.build());
String etag = response.etag() == null ? "" : response.etag().replace("\"", "");
return new PartEtagVO(etag, partNumber);
} catch (ServiceException serviceException) {
throw serviceException;
} catch (Exception exception) {
log.error("MinIO分段上传异常 objectKey:{} partNumber:{}", objectKey, partNumber, exception);
throw new ServiceException("MinIO分段上传异常");
}
}
@Override
public void cancelMultipartUpload(String objectKey, String uploadId) {
// MinIO 分片以独立对象模拟,取消时由任务层清理业务状态即可
log.debug("MinIO取消分段上传 objectKey:{} uploadId:{}", objectKey, uploadId);
}
@Override
public void completeMultipartUpload(String objectKey, String uploadId, List<org.springblade.file.pojo.vo.PartEtagVO> partEtags) {
try {
+3 -2
View File
@@ -120,11 +120,12 @@ baidu:
request-timeout: 30s
token-refresh-advance: 1m
# 凭证分片上传等走 blade-file,外网地址需单独配置(与 /resource/oss 的 transformEndpoint 保持一致)
# 凭证分片上传走 blade-file,与 /resource/oss 库表配置相互独立;外网地址必须在此配置并发布到 Nacos 后重启 blade-file
# 不要使用可被空环境变量覆盖的 ${VAR:default},否则仍会签发 http://内网IP 预签名导致 Mixed Content
file:
storage:
minio:
transform-endpoint: ${MINIO_TRANSFORM_ENDPOINT:https://tms-prod.xinghy56.com/oss}
transform-endpoint: https://tms-prod.xinghy56.com/oss
powerjob:
worker: