From 7e8cc228597f93183bc688f8fb097fbd4af54b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8E=86=E5=86=B0=E9=83=81-hermes=E7=89=88?= Date: Fri, 8 May 2026 14:28:04 +0800 Subject: [PATCH] fix oss object key guidance --- .hermes/shared-memory/pitfalls.md | 8 ++++---- server-rs/crates/platform-oss/README.md | 4 ++-- server-rs/crates/platform-oss/src/lib.rs | 21 +++++---------------- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 7dd7f26b..266e8e0b 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -16,10 +16,10 @@ ## OSS V4 签名时间和 bucket/object_key 兼容 -- 现象:使用 `xushi-dev/generated-square-hole-assets/.../image.png` 这类带 bucket 前缀的对象路径做私有读签名时,请求可能被判定为不在 `generated-*` 白名单下;小时小于 10 时还可能出现 `OSS V4 签名时间格式化失败` 或服务端签名格式错误。 -- 原因:旧逻辑直接把完整路径当 `object_key` 校验,未剥离当前配置 bucket;同时依赖 `time::Time::to_string()` 再去掉冒号,小时小于 10 时输出不稳定补零。 -- 处理:在 `platform-oss` 内统一用 `normalize_object_key_for_bucket` 先剥离当前 bucket 前缀,再做 object_key 白名单校验;OSS V4 `x-oss-date` 使用固定宽度 `yyyyMMdd'T'HHmmss'Z'` 格式化。 -- 验证:运行 `cd server-rs && cargo test -p platform-oss -- --nocapture`,并覆盖 `xushi-dev/generated-square-hole-assets/square-hole-session-546d881972684be2980a2a882cd0cc71/square-hole-profile-134411276ce1469cbe398f946a25d7f8/square-hole-shape-image/rabbit-option/asset-1777979289912039/image.png`。 +- 现象:OSS V4 私有读签名在部分时间点失败,可能出现 `OSS V4 签名时间格式化失败` 或服务端判定签名格式错误;排查用例中 bucket 为 `xushi-dev`,object_key 为 `generated-square-hole-assets/.../image.png`。 +- 原因:旧逻辑依赖 `time::Time::to_string()` 再去掉冒号,小时小于 10 时输出不稳定补零;同时排查时容易把 bucket 名误当成 object_key 的一部分。 +- 处理:OSS V4 `x-oss-date` 使用固定宽度 `yyyyMMdd'T'HHmmss'Z'` 格式化;调用读签名或 `HEAD Object` 时只传 object_key,不要传 `bucket/object_key` 拼接路径。 +- 验证:运行 `cd server-rs && cargo test -p platform-oss -- --nocapture`,并用 bucket=`xushi-dev`、object_key=`generated-square-hole-assets/square-hole-session-546d881972684be2980a2a882cd0cc71/square-hole-profile-134411276ce1469cbe398f946a25d7f8/square-hole-shape-image/rabbit-option/asset-1777979289912039/image.png` 覆盖签名生成。 - 关联:`server-rs/crates/platform-oss/src/lib.rs`、`server-rs/crates/platform-oss/README.md`。 ## 中文乱码与编码风险 diff --git a/server-rs/crates/platform-oss/README.md b/server-rs/crates/platform-oss/README.md index c33c8d7c..025481d3 100644 --- a/server-rs/crates/platform-oss/README.md +++ b/server-rs/crates/platform-oss/README.md @@ -34,8 +34,8 @@ 1. 当前产品口径为服务器上传 AI 生成资源、Web 端只负责读取。 2. 因此 `STS` 不作为默认上传主链,`api-server` 只暴露禁用式 contract,避免浏览器拿到 OSS 写权限。 3. 服务端生成资源应优先复用 `OssClient::put_object`,上传成功后再走对象确认链路写入 `asset_object`。 -4. 读签名和 `HEAD Object` 的入参可以兼容 `bucket/object_key` 形式;例如 `xushi-dev/generated-square-hole-assets/.../image.png` 会先剥离当前配置 bucket,再参与 OSS V4 canonical request。若 bucket 与当前配置不一致,仍按普通 object_key 校验并拒绝不在 `generated-*` 前缀下的路径。 -5. OSS V4 `x-oss-date` 必须固定为 `yyyyMMdd'T'HHmmss'Z'`,不能依赖 `time::Time::to_string()`;后者在小时小于 10 时可能输出非补零时间,导致签名格式错误。 + 4. 读签名和 `HEAD Object` 的入参必须直接传 object_key,不要把 bucket 名拼进路径;例如 `generated-square-hole-assets/.../image.png` 才是正确入参,`xushi-dev/...` 这类前缀不属于 object_key。 + 5. OSS V4 `x-oss-date` 必须固定为 `yyyyMMdd'T'HHmmss'Z'`,不能依赖 `time::Time::to_string()`;后者在小时小于 10 时可能输出非补零时间,导致签名格式错误。 ## 3. 边界约束 diff --git a/server-rs/crates/platform-oss/src/lib.rs b/server-rs/crates/platform-oss/src/lib.rs index bbb0dd68..27cf0c65 100644 --- a/server-rs/crates/platform-oss/src/lib.rs +++ b/server-rs/crates/platform-oss/src/lib.rs @@ -468,7 +468,7 @@ impl OssClient { )); } - let object_key = normalize_object_key_for_bucket(&self.config.bucket, &request.object_key)?; + let object_key = normalize_object_key(&request.object_key)?; let expires_at = OffsetDateTime::now_utc() .checked_add(Duration::seconds(i64::try_from(expire_seconds).map_err( |_| OssError::InvalidRequest("expireSeconds 超出可支持范围".to_string()), @@ -541,7 +541,7 @@ impl OssClient { client: &reqwest::Client, request: OssHeadObjectRequest, ) -> Result { - let object_key = normalize_object_key_for_bucket(&self.config.bucket, &request.object_key)?; + let object_key = normalize_object_key(&request.object_key)?; let target_url = build_object_url(&self.config.bucket, &self.config.endpoint, &object_key) .map_err(|error| OssError::Request(format!("构造 OSS 对象 URL 失败:{error}")))?; let response = send_signed_request( @@ -764,17 +764,6 @@ fn build_object_key( parts.join("/") } -fn normalize_object_key_for_bucket(bucket: &str, raw: &str) -> Result { - let normalized = raw.trim().trim_start_matches('/').trim().to_string(); - let bucket_prefix = format!("{}/", bucket.trim().trim_matches('/')); - let object_key = normalized - .strip_prefix(&bucket_prefix) - .unwrap_or(normalized.as_str()) - .to_string(); - - normalize_object_key(&object_key) -} - fn normalize_object_key(raw: &str) -> Result { let normalized = raw.trim().trim_start_matches('/').trim().to_string(); if normalized.is_empty() { @@ -1470,7 +1459,7 @@ mod tests { } #[test] - fn sign_get_object_url_accepts_bucket_prefixed_square_hole_key() { + fn sign_get_object_url_uses_square_hole_object_key_without_bucket_prefix() { let client = OssClient::new( OssConfig::new( "xushi-dev".to_string(), @@ -1487,10 +1476,10 @@ mod tests { let response = client .sign_get_object_url(OssSignedGetObjectUrlRequest { - object_key: "xushi-dev/generated-square-hole-assets/square-hole-session-546d881972684be2980a2a882cd0cc71/square-hole-profile-134411276ce1469cbe398f946a25d7f8/square-hole-shape-image/rabbit-option/asset-1777979289912039/image.png".to_string(), + object_key: "generated-square-hole-assets/square-hole-session-546d881972684be2980a2a882cd0cc71/square-hole-profile-134411276ce1469cbe398f946a25d7f8/square-hole-shape-image/rabbit-option/asset-1777979289912039/image.png".to_string(), expire_seconds: Some(300), }) - .expect("bucket-prefixed square hole key should build signed url"); + .expect("square hole object key should build signed url"); assert_eq!(response.bucket, "xushi-dev".to_string()); assert_eq!(