feat(infra): S3 私有桶预签名核心能力
- S3FileClient.buildDomain() 修复 COS virtual-hosted-style 域名生成 - S3FileClient.presignGetUrl() 支持跨桶签名及 endpoint 校验 - FileApi.presignGetUrl() 修复 Feign nullable 参数注解 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,6 @@ public interface FileApi {
|
||||
@GetMapping(PREFIX + "/presigned-url")
|
||||
@Operation(summary = "生成文件预签名地址,用于读取")
|
||||
CommonResult<String> presignGetUrl(@NotEmpty(message = "URL 不能为空") @RequestParam("url") String url,
|
||||
Integer expirationSeconds);
|
||||
@RequestParam(value = "expirationSeconds", required = false) Integer expirationSeconds);
|
||||
|
||||
}
|
||||
|
||||
@@ -114,37 +114,83 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
||||
|
||||
@Override
|
||||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||
// 1. 将 url 转换为 path
|
||||
// 1. 将 url 转换为 path 和 bucket
|
||||
String bucket = config.getBucket();
|
||||
boolean crossBucket = false;
|
||||
String path = StrUtil.removePrefix(url, config.getDomain() + "/");
|
||||
// 兼容:域名不匹配时(如同账号不同桶),从 URL 中解析 bucket 和 path
|
||||
if (path.equals(url) && (path.startsWith("http://") || path.startsWith("https://"))) {
|
||||
try {
|
||||
URI uri = URI.create(url);
|
||||
String host = uri.getHost();
|
||||
String endpointHost = getEndpointHost();
|
||||
// 校验是否同一 S3 服务(如 cos.ap-shanghai.myqcloud.com),避免误签非本服务 URL
|
||||
if (host != null && endpointHost != null && host.endsWith("." + endpointHost)) {
|
||||
bucket = StrUtil.subBefore(host, ".", false);
|
||||
crossBucket = true;
|
||||
path = StrUtil.removePrefix(uri.getPath(), "/");
|
||||
} else {
|
||||
// 非同服务 URL,原样返回
|
||||
return url;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
path = HttpUtils.decodeUtf8(HttpUtils.removeUrlQuery(path));
|
||||
|
||||
// 2.1 情况一:公开访问:无需签名
|
||||
// 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名
|
||||
if (!BooleanUtil.isFalse(config.getEnablePublicAccess())) {
|
||||
// 跨桶 URL 原样返回,不拼接本桶 domain
|
||||
if (crossBucket) {
|
||||
return url;
|
||||
}
|
||||
return config.getDomain() + "/" + path;
|
||||
}
|
||||
|
||||
// 2.2 情况二:私有访问:生成 GET 预签名 URL
|
||||
String finalPath = path;
|
||||
String finalBucket = bucket;
|
||||
Duration expiration = expirationSeconds != null ? Duration.ofSeconds(expirationSeconds) : EXPIRATION_DEFAULT;
|
||||
URL signedUrl = presigner.presignGetObject(GetObjectPresignRequest.builder()
|
||||
.signatureDuration(expiration)
|
||||
.getObjectRequest(b -> b.bucket(config.getBucket()).key(finalPath)).build())
|
||||
.getObjectRequest(b -> b.bucket(finalBucket).key(finalPath)).build())
|
||||
.url();
|
||||
return signedUrl.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 endpoint 的 host 部分(不含协议头)
|
||||
* 例如:https://cos.ap-shanghai.myqcloud.com → cos.ap-shanghai.myqcloud.com
|
||||
*/
|
||||
private String getEndpointHost() {
|
||||
String endpoint = config.getEndpoint();
|
||||
if (HttpUtil.isHttp(endpoint) || HttpUtil.isHttps(endpoint)) {
|
||||
try {
|
||||
return URI.create(endpoint).getHost();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 bucket + endpoint 构建访问的 Domain 地址
|
||||
*
|
||||
* @return Domain 地址
|
||||
*/
|
||||
private String buildDomain() {
|
||||
// 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO
|
||||
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
|
||||
// Path-Style(如 MinIO):{endpoint}/{bucket}
|
||||
if (Boolean.TRUE.equals(config.getEnablePathStyleAccess())) {
|
||||
return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
|
||||
}
|
||||
// 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名
|
||||
// Virtual-Hosted-Style(如腾讯云 COS、阿里云 OSS):{scheme}://{bucket}.{host}
|
||||
URI uri = URI.create(config.getEndpoint());
|
||||
return StrUtil.format("{}://{}.{}", uri.getScheme(), config.getBucket(), uri.getHost());
|
||||
}
|
||||
// 没有协议头,直接拼接为虚拟主机风格
|
||||
return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user