🐛 修复凭证分片上传 Mixed Content,支持服务端代传兜底
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package org.springblade.file.config;
|
|||||||
import com.aliyun.oss.OSSClientBuilder;
|
import com.aliyun.oss.OSSClientBuilder;
|
||||||
import com.obs.services.ObsClient;
|
import com.obs.services.ObsClient;
|
||||||
import io.minio.MinioClient;
|
import io.minio.MinioClient;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springblade.core.log.exception.ServiceException;
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.tool.utils.StringUtil;
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
import org.springblade.file.service.IFileService;
|
import org.springblade.file.service.IFileService;
|
||||||
@@ -20,6 +21,7 @@ import org.springframework.core.env.Environment;
|
|||||||
* @author bfhuange
|
* @author bfhuange
|
||||||
* @date 2024/10/21
|
* @date 2024/10/21
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
public class FileConfig {
|
public class FileConfig {
|
||||||
|
|
||||||
@@ -63,6 +65,15 @@ public class FileConfig {
|
|||||||
|
|
||||||
private IFileService buildMinioFileService(FileStorageProperties.StoreProperties properties) {
|
private IFileService buildMinioFileService(FileStorageProperties.StoreProperties properties) {
|
||||||
validateStoreProperties(STORAGE_MINIO, 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()
|
MinioClient minioClient = MinioClient.builder()
|
||||||
.endpoint(properties.getEndpoint())
|
.endpoint(properties.getEndpoint())
|
||||||
.credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
|
.credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
|
||||||
@@ -95,12 +106,15 @@ public class FileConfig {
|
|||||||
FileStorageProperties.StoreProperties mergedProperties = new FileStorageProperties.StoreProperties();
|
FileStorageProperties.StoreProperties mergedProperties = new FileStorageProperties.StoreProperties();
|
||||||
String fileStoragePrefix = "file.storage." + prefix;
|
String fileStoragePrefix = "file.storage." + prefix;
|
||||||
mergedProperties.setEndpoint(firstNotBlank(properties.getEndpoint(), environment.getProperty(fileStoragePrefix + ".endpoint"), environment.getProperty(prefix + ".endpoint")));
|
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(
|
mergedProperties.setTransformEndpoint(firstNotBlank(
|
||||||
properties.getTransformEndpoint(),
|
properties.getTransformEndpoint(),
|
||||||
environment.getProperty(fileStoragePrefix + ".transform-endpoint"),
|
environment.getProperty(fileStoragePrefix + ".transform-endpoint"),
|
||||||
|
environment.getProperty(fileStoragePrefix + ".transformEndpoint"),
|
||||||
environment.getProperty(prefix + ".transform-endpoint"),
|
environment.getProperty(prefix + ".transform-endpoint"),
|
||||||
// 兼容 blade-resource / oss 配置,避免仅改对象存储外网地址时分片上传仍走内网 HTTP
|
environment.getProperty(prefix + ".transformEndpoint"),
|
||||||
environment.getProperty("oss.transform-endpoint")
|
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.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.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) {
|
private String firstNotBlank(String... values) {
|
||||||
for (String value : values) {
|
for (String value : values) {
|
||||||
if (StringUtil.isNotBlank(value)) {
|
if (StringUtil.isNotBlank(value)) {
|
||||||
return value;
|
return value.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+22
@@ -111,4 +111,26 @@ public class FileTaskController extends BladeController {
|
|||||||
String uploadUrl = fileTaskService.getUploadUrl(id, partNumber);
|
String uploadUrl = fileTaskService.getUploadUrl(id, partNumber);
|
||||||
return R.data(uploadUrl);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -74,6 +74,16 @@ public interface IFileTaskService extends IService<FileTask> {
|
|||||||
*/
|
*/
|
||||||
String getUploadUrl(Long id, Integer partNumber);
|
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
|
* @param taskIds
|
||||||
|
|||||||
+27
@@ -259,6 +259,33 @@ public class FileTaskServiceImpl extends ServiceImpl<FileTaskMapper, FileTask> i
|
|||||||
return fileService.getUploadPartUrl(fileTask.getObjectKey(), fileTask.getUploadId(), partNumber, null);
|
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)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@Override
|
@Override
|
||||||
public void cancelBatchByIds(List<Long> taskIds) {
|
public void cancelBatchByIds(List<Long> taskIds) {
|
||||||
|
|||||||
+33
@@ -30,6 +30,7 @@ import io.minio.ComposeSource;
|
|||||||
import io.minio.GetObjectArgs;
|
import io.minio.GetObjectArgs;
|
||||||
import io.minio.GetPresignedObjectUrlArgs;
|
import io.minio.GetPresignedObjectUrlArgs;
|
||||||
import io.minio.MinioClient;
|
import io.minio.MinioClient;
|
||||||
|
import io.minio.ObjectWriteResponse;
|
||||||
import io.minio.PutObjectArgs;
|
import io.minio.PutObjectArgs;
|
||||||
import io.minio.RemoveObjectArgs;
|
import io.minio.RemoveObjectArgs;
|
||||||
import io.minio.UploadObjectArgs;
|
import io.minio.UploadObjectArgs;
|
||||||
@@ -39,8 +40,10 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springblade.core.log.exception.ServiceException;
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.tool.utils.StringUtil;
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
import org.springblade.file.config.FileStorageProperties;
|
import org.springblade.file.config.FileStorageProperties;
|
||||||
|
import org.springblade.file.pojo.vo.PartEtagVO;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.file.Files;
|
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
|
@Override
|
||||||
public void completeMultipartUpload(String objectKey, String uploadId, List<org.springblade.file.pojo.vo.PartEtagVO> partEtags) {
|
public void completeMultipartUpload(String objectKey, String uploadId, List<org.springblade.file.pojo.vo.PartEtagVO> partEtags) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -120,11 +120,12 @@ baidu:
|
|||||||
request-timeout: 30s
|
request-timeout: 30s
|
||||||
token-refresh-advance: 1m
|
token-refresh-advance: 1m
|
||||||
|
|
||||||
# 凭证分片上传等走 blade-file,外网地址需单独配置(与 /resource/oss 的 transformEndpoint 保持一致)
|
# 凭证分片上传走 blade-file,与 /resource/oss 库表配置相互独立;外网地址必须在此配置并发布到 Nacos 后重启 blade-file
|
||||||
|
# 不要使用可被空环境变量覆盖的 ${VAR:default},否则仍会签发 http://内网IP 预签名导致 Mixed Content
|
||||||
file:
|
file:
|
||||||
storage:
|
storage:
|
||||||
minio:
|
minio:
|
||||||
transform-endpoint: ${MINIO_TRANSFORM_ENDPOINT:https://tms-prod.xinghy56.com/oss}
|
transform-endpoint: https://tms-prod.xinghy56.com/oss
|
||||||
|
|
||||||
powerjob:
|
powerjob:
|
||||||
worker:
|
worker:
|
||||||
|
|||||||
Reference in New Issue
Block a user