From e48f8ce92691980879a088da17b5433354a769cb Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Fri, 23 Oct 2026 10:12:07 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20COS=20=E7=AD=BE=E5=90=8D=20URL=20?= =?UTF-8?q?=E8=BF=87=E6=9C=9F=E6=97=B6=E6=8C=89=20object=20key=20=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E7=AD=BE=E5=8F=91=E8=AE=BF=E9=97=AE=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 历史库内若存带 q-sign 的完整 COS 链接,打印/查看会 AccessDenied;写入只保留 key,读出时重新签名。 Co-authored-by: Cursor --- .../Storage/FileStoragePath.cs | 31 +++++++++ .../Storage/OmsObjectStorageExtensions.cs | 17 ++++- src/Oms.Tests/FileStoragePathTests.cs | 67 +++++++++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/src/Oms.Application/Storage/FileStoragePath.cs b/src/Oms.Application/Storage/FileStoragePath.cs index 45cae8f..a9af070 100644 --- a/src/Oms.Application/Storage/FileStoragePath.cs +++ b/src/Oms.Application/Storage/FileStoragePath.cs @@ -39,6 +39,37 @@ public static class FileStoragePath && (value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)); + /// + /// 是否为本系统对象存储域名(自定义域 cos.* / 腾讯云默认域)。 + /// 这类地址带临时签名,库内应存 object key,读出时重新签名;不可当外链原样返回。 + /// + public static bool IsManagedObjectStorageUrl(string? value) + { + if (!IsAbsoluteUrl(value)) + { + return false; + } + + try + { + var host = new Uri(value!).Host; + if (string.IsNullOrWhiteSpace(host)) + { + return false; + } + + host = host.ToLowerInvariant(); + return host.StartsWith("cos.", StringComparison.Ordinal) + || host.Contains("myqcloud.com", StringComparison.Ordinal) + || host.Contains("tencentcos.cn", StringComparison.Ordinal) + || host.Contains("tencentcos.com", StringComparison.Ordinal); + } + catch (UriFormatException) + { + return false; + } + } + /// /// 读取对象时尝试的 key:含/不含 admin/ 前缀(自定义域路径常带 PathBase)。 /// diff --git a/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs b/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs index 28910bc..20db8cc 100644 --- a/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs +++ b/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs @@ -8,7 +8,7 @@ namespace Oms.Application.Storage; public static class OmsObjectStorageExtensions { /// - /// 将 key / 相对路径规范为库内 object key;绝对外链原样返回;空返回 null。 + /// 将 key / 相对路径规范为库内 object key;本系统 COS 签名 URL 剥签后存 key;其它绝对外链原样返回;空返回 null。 /// public static string? ToStorageKey(string? keyOrUrl) { @@ -23,6 +23,12 @@ public static class OmsObjectStorageExtensions return "0"; } + // 私有桶签名 URL 会过期,禁止写入库;只保留 object key。 + if (FileStoragePath.IsManagedObjectStorageUrl(raw)) + { + return FileStoragePath.NormalizeKey(raw); + } + if (FileStoragePath.IsAbsoluteUrl(raw)) { return raw; @@ -32,7 +38,7 @@ public static class OmsObjectStorageExtensions } /// - /// 临时访问地址。空值返回空串;外链原样返回;逻辑 key 走 。 + /// 临时访问地址。空值返回空串;真正外链原样返回;逻辑 key / 本系统 COS URL 走 重新签名。 /// Local 存储目录不可写/不可读时降级返回 key,避免拖垮下单等主流程。 /// public static async Task GetPublicUrlAsync( @@ -46,7 +52,12 @@ public static class OmsObjectStorageExtensions } var raw = keyOrUrl.Trim(); - if (FileStoragePath.IsAbsoluteUrl(raw)) + // 历史数据可能把带 q-sign 的完整 URL 存进库;直接返回会 AccessDenied/Request has expired。 + if (FileStoragePath.IsManagedObjectStorageUrl(raw)) + { + raw = FileStoragePath.NormalizeKey(raw); + } + else if (FileStoragePath.IsAbsoluteUrl(raw)) { return raw; } diff --git a/src/Oms.Tests/FileStoragePathTests.cs b/src/Oms.Tests/FileStoragePathTests.cs index 07b0843..9a26ce1 100644 --- a/src/Oms.Tests/FileStoragePathTests.cs +++ b/src/Oms.Tests/FileStoragePathTests.cs @@ -1,4 +1,6 @@ +using Fake.ObjectStorage; using FluentAssertions; +using Moq; using Oms.Application.Storage; namespace Oms.Tests; @@ -16,4 +18,69 @@ public class FileStoragePathTests keys.Should().Contain("admin/uploads/prints/orders/IDE2610061059117673-20261006105912.pdf"); keys.Should().Contain("uploads/prints/orders/IDE2610061059117673-20261006105912.pdf"); } + + [Theory] + [InlineData("https://cos.ideship.com/uploads/a.pdf?q-sign-algorithm=sha1", true)] + [InlineData("http://cos.test.ideship.com/admin/uploads/a.pdf", true)] + [InlineData("https://bucket-123.cos.ap-guangzhou.myqcloud.com/uploads/a.pdf", true)] + [InlineData("https://third-party.example.com/label.pdf", false)] + [InlineData("uploads/prints/orders/a.pdf", false)] + public void IsManagedObjectStorageUrl_ShouldDetectCosHosts(string value, bool expected) + { + FileStoragePath.IsManagedObjectStorageUrl(value).Should().Be(expected); + } + + [Fact] + public void ToStorageKey_ShouldStripExpiredCosSignedUrlToObjectKey() + { + var expired = + "https://cos.ideship.com/uploads/prints/orders/IDE2610081704036507-20261008170404.pdf?q-sign-algorithm=sha1&q-sign-time=1;2"; + + OmsObjectStorageExtensions.ToStorageKey(expired) + .Should() + .Be("uploads/prints/orders/IDE2610081704036507-20261008170404.pdf"); + } + + [Fact] + public void ToStorageKey_ShouldKeepExternalAbsoluteUrl() + { + const string external = "https://third-party.example.com/label.pdf?token=1"; + OmsObjectStorageExtensions.ToStorageKey(external).Should().Be(external); + } + + [Fact] + public async Task GetPublicUrl_ShouldResignManagedCosAbsoluteUrl() + { + var expired = + "https://cos.ideship.com/uploads/prints/orders/IDE2610081704036507-20261008170404.pdf?q-sign-algorithm=sha1&q-sign-time=1;2"; + const string fresh = + "https://cos.ideship.com/uploads/prints/orders/IDE2610081704036507-20261008170404.pdf?q-sign-algorithm=sha1&fresh=1"; + + var storage = new Mock(MockBehavior.Strict); + storage + .Setup(x => x.GetUrlAsync( + "uploads/prints/orders/IDE2610081704036507-20261008170404.pdf", + It.IsAny(), + It.IsAny())) + .ReturnsAsync(fresh); + + var url = await storage.Object.GetPublicUrlAsync(expired); + + url.Should().Be(fresh); + storage.VerifyAll(); + } + + [Fact] + public async Task GetPublicUrl_ShouldPassThroughExternalAbsoluteUrl() + { + const string external = "https://third-party.example.com/label.pdf"; + var storage = new Mock(MockBehavior.Strict); + + var url = await storage.Object.GetPublicUrlAsync(external); + + url.Should().Be(external); + storage.Verify( + x => x.GetUrlAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } } -- 2.45.2