S3、OSS 与 COS 兼容接入、权限和费用工具手册
一次“上传成功、业务失败”的现场
用户上传头像时,浏览器拿到了 200 OK,业务库里也写入了对象地址;几分钟后读取却返回 403。开发者把 AWS S3 的 endpoint 换成 OSS 域名仍然失败,又把 path-style 打开,错误从 SignatureDoesNotMatch 变成 NoSuchBucket。这不是一个“网络偶发问题”,而是五个对象被混在了一起:
bucket 是对象的管理边界,绑定地域、权限、版本、生命周期和费用。key 是桶内对象名,不是文件系统绝对路径;dev/avatar/u-42/a.png 只是一个字符串。region 决定请求应发往哪里,也进入签名或 SDK 配置。
endpoint 是网络入口;公网、同云内网、自定义域名和加速域名可能指向同一批对象,却有不同网络与费用路径。credential 决定调用者是谁、能做什么、能做多久;预签名 URL 只是把一次受限授权编码进 URL。
对象存储接入的第一条不变量因此是:业务库保存的是 provider + bucket + key + versionId/etag,不是一条不可解释、不可迁移的完整 URL。URL 可以重新生成,数据归属和对象身份不能靠 URL 猜。
接下来的操作只使用独立开发账号、开发桶和脱敏小文件。三家都是托管服务,开发者要启用的是一只隔离 bucket、一个受限身份和对应 CLI,而不是在本机启动存储服务。bucket 会产生请求、存储和流量费用,也可能受组织策略约束,因此应由资源 owner 分配;不要为了跟做命令临时创建无人负责的桶。
AWS CLI、ossutil 2.0、COSCLI 三者至少安装一个;做三云契约测试时再装齐。安装包只从 AWS CLI 安装入口、ossutil 安装入口和 COSCLI 下载与安装配置获取,并先确认实际版本:
aws --version
ossutil --version
coscli --versionAWS 开发者优先用 aws configure sso --profile your-dev-profile 建立短期会话;ossutil 2.0 用 ossutil config 建立独立 profile,并确保 region、endpoint 与 STS token 同属目标开发环境;COSCLI 用 coscli config init 建立独立配置,填入完整 BucketName-APPID、region 和临时 token。配置完成后先执行只读身份或桶探测,不要把“能列整个账号的 bucket”当成成功证据。长期 AK/SK 不写进命令历史和 .env,优先使用 SSO、角色或 STS 临时凭证。测试 key 固定在 dev/object-lab/$USER/,这样清理动作不会碰整桶。
先把三家地址写对
三家的客户端都能完成 PUT、HEAD、GET、DELETE,但地址不是同一种字符串模板。
| 平台 | bucket 与主机名 | region / endpoint 要点 | 凭证字段 |
|---|---|---|---|
| AWS S3 | bucket.s3.region.amazonaws.com | 新接入使用 regional endpoint 和 virtual-hosted style;path-style 仍受支持,但处于未来停止方向 | AccessKeyId、SecretAccessKey、SessionToken |
| 阿里云 OSS | bucket.oss-region.aliyuncs.com | 公网、内网、双栈 endpoint 不同;V4 签名中的 region 必须与桶匹配 | AccessKeyId、AccessKeySecret、SecurityToken |
| 腾讯云 COS | BucketName-APPID.cos.Region.myqcloud.com | bucket 参数必须带 APPID;默认域名直接包含地域简称 | TmpSecretId、TmpSecretKey、Token |
AWS 的 virtual-hosted 与 path-style 规则还揭示一个容易忽略的 TLS 约束:virtual-hosted style 使用 HTTPS 时,通配符证书不匹配包含点号的 bucket 名。阿里云应从 OSS 地域与 Endpoint 表选择公网或同地域内网入口,不能凭 region 字符串自行猜特殊云环境的域名。腾讯云的 COS 地域和访问域名表给出的默认域名以 <BucketName-APPID>.cos.<Region>.myqcloud.com 为准;同地域 CVM 解析为内网地址时不收流量费,但请求次数仍可能计费。
项目配置要保留这些差异,而不是只留一个含义模糊的 S3_ENDPOINT:
OBJECT_STORE_PROVIDER=aws-s3
OBJECT_STORE_REGION=us-east-1
OBJECT_STORE_BUCKET=your-project-dev
OBJECT_STORE_PREFIX=dev/object-lab/local-user/
OBJECT_STORE_ENDPOINT=
OBJECT_STORE_ADDRESSING_STYLE=virtual-hosted
OBJECT_STORE_CREDENTIAL_SOURCE=default-chain
OBJECT_STORE_PRESIGN_TTL_SECONDS=600
OBJECT_STORE_MAX_BYTES=10485760真实云的标准 endpoint 通常由 SDK 根据 provider 和 region 推导;OBJECT_STORE_ENDPOINT 留空。只有本地模拟器、私有兼容服务、专有云或明确的内网入口才覆盖它。OBJECT_STORE_ADDRESSING_STYLE=path 也只应是兼容端的显式能力,不应成为所有云的默认开关。
用最小权限跑通四步闭环
先创建本地证据文件并记录摘要。摘要比 ETag 更适合做跨云契约:单段上传时 ETag 常与 MD5 有关系,但 multipart、加密和厂商实现会改变它的含义,不能把 ETag 永久解释成 MD5。
printf 'object-storage-contract-v1\n' > hello.txt
sha256sum hello.txt
# 预期格式:<64 位十六进制摘要> hello.txtAWS S3:身份、地域、对象状态逐项确认
开发者身份至少需要目标 prefix 上的 s3:PutObject、s3:GetObject、s3:DeleteObject,以及桶级、受 prefix 条件限制的 s3:ListBucket。若只做 HEAD,AWS 仍按读取对象权限判断。新建 S3 bucket 默认采用 Bucket owner enforced,ACL 被禁用,访问控制应落到 IAM、bucket policy、VPC endpoint policy 和组织级策略;S3 Object Ownership给出了这一默认行为。
export AWS_PROFILE=your-dev-profile
export AWS_REGION=us-east-1
export OBJECT_STORE_BUCKET=your-project-dev
export OBJECT_STORE_PREFIX=dev/object-lab/local-user/
export OBJECT_KEY="${OBJECT_STORE_PREFIX}hello.txt"
aws sts get-caller-identity
aws s3api head-bucket --bucket "$OBJECT_STORE_BUCKET"
aws s3api put-object --bucket "$OBJECT_STORE_BUCKET" --key "$OBJECT_KEY" --body hello.txt
aws s3api head-object --bucket "$OBJECT_STORE_BUCKET" --key "$OBJECT_KEY"
aws s3api get-object --bucket "$OBJECT_STORE_BUCKET" --key "$OBJECT_KEY" downloaded.txt
sha256sum hello.txt downloaded.txt
aws s3api delete-object --bucket "$OBJECT_STORE_BUCKET" --key "$OBJECT_KEY"
aws s3api head-object --bucket "$OBJECT_STORE_BUCKET" --key "$OBJECT_KEY"正向证据不是单独一个 200。get-caller-identity 应返回预期账号和角色;head-bucket 成功,并可从调试输出或响应中的 bucket region 核对 AWS_REGION;head-object 返回 ContentLength、ETag 和服务端加密字段;两个 SHA-256 相同;最后一次 HEAD 应返回 404。如果最后得到 403,当前身份可能缺少列桶权限,不能据此断言对象仍存在。AWS 已把 HeadBucket 作为地域探测入口,它即使在拒绝访问时也会返回正确地域响应头;普通开发链路不再依赖 GetBucketLocation。
S3 对 PUT、覆盖、DELETE、GET、HEAD 和 LIST 提供强一致性,因此成功 PUT 后立即 HEAD 不需要用“等几秒再试”掩盖错误,详见 S3 数据一致性模型。
阿里云 OSS:让 endpoint 和 V4 region 成对出现
ossutil 使用独立 profile 或环境注入的临时凭证。若 STS 请求还传了会话 policy,最终权限是 RAM 角色权限与会话 policy 的交集;给会话 policy 增权不会突破角色上限,OSS STS 授权说明明确了这个关系。
export OSS_BUCKET=your-project-dev
export OBJECT_PREFIX=dev/object-lab/local-user/
export OSS_URI="oss://${OSS_BUCKET}/${OBJECT_PREFIX}hello.txt"
ossutil ls "oss://${OSS_BUCKET}/${OBJECT_PREFIX}"
ossutil cp hello.txt "$OSS_URI"
ossutil stat "$OSS_URI"
ossutil cp "$OSS_URI" downloaded.txt
sha256sum hello.txt downloaded.txt
ossutil rm "$OSS_URI"
ossutil stat "$OSS_URI"前五步应依次得到可访问的 prefix、上传成功、对象长度与 ETag、相同摘要和删除成功;最后一次 stat 应明确报对象不存在。若 profile 选择了 oss-cn-hangzhou.aliyuncs.com,V4 signer 的 region 也必须是 cn-hangzhou。同地域云主机可换内网 endpoint,但切换后要用 DNS、一次上传和账单维度同时确认网络路径,而不是只看请求成功。
腾讯云 COS:bucket 名必须携带 APPID
COSCLI 的 profile 绑定开发环境临时密钥和地域。腾讯云 STS 返回 TmpSecretId、TmpSecretKey、Token;SDK 初始化时三者缺一不可。COS 临时密钥指引还要求通过 allowPrefixes 和 allowActions 收窄资源与操作,示例中的 * 不能直接进入业务环境。
export COS_BUCKET=your-project-dev-1250000000
export OBJECT_PREFIX=dev/object-lab/local-user/
export COS_URI="cos://${COS_BUCKET}/${OBJECT_PREFIX}hello.txt"
coscli ls "cos://${COS_BUCKET}/${OBJECT_PREFIX}"
coscli cp hello.txt "$COS_URI"
coscli stat "$COS_URI"
coscli cp "$COS_URI" downloaded.txt
sha256sum hello.txt downloaded.txt
coscli rm "$COS_URI"
coscli stat "$COS_URI"如果把 your-project-dev-1250000000 错写成 your-project-dev,修复动作不是追加一个 AWS 风格 endpoint,而是使用控制台显示的完整 bucket 名,并核对 COS region。四步闭环的状态证据与前两家相同:上传、HEAD/STAT、下载摘要、删除后不存在。
反向实验:稳定制造 region 与签名错误
正向命令只能证明一组配置可用;反向实验才能说明失败由哪条约束触发。以下操作都针对测试 key,不更改桶级策略。
错 region 会在鉴权前后留下不同证据
对一个不在 us-east-1 的 S3 bucket,故意向错误地域 endpoint 发 HEAD:
curl -sS -D headers.txt -o body.txt \
"https://${OBJECT_STORE_BUCKET}.s3.us-east-1.amazonaws.com/${OBJECT_KEY}" || true
sed -n '1,20p' headers.txt私有桶不会因为没有签名就返回对象,但响应头仍可能给出 x-amz-bucket-region,或响应表现为重定向 / AuthorizationHeaderMalformed。这条证据先证明路由配置错了;随后再用正确 profile 执行 head-bucket,必要时加 --debug 查看响应头,不要在一个 AccessDenied 上反复扩大权限。HeadBucket 官方说明明确区分了“地域响应头可见”和“调用者有权访问 bucket”这两件事。
OSS 的等价反例是让 client region 与 endpoint 不一致,COS 则是把 SDK Region 改成另一个地域。三家都应记录 HTTP 状态、错误码、请求 ID、实际 host 和 SDK 配置的 region。修复后重跑同一个 HEAD,只有从“确定性失败”变成“返回对象元数据”才算闭环。
改一个已签名 header,PUT 必须失败
预签名不是“带 token 的普通 URL”。以 SigV4 / OSS V4 为例,SDK会把 HTTP method、规范化 URI、排序后的 query、规范化 header、signed headers 和 payload hash 组成 canonical request,再对 credential scope 和时间派生出的签名材料计算摘要。请求中的 method、host 或被签名 header 变化,服务端重建出的 canonical request 就不同。
HTTP Method
Canonical URI
Canonical Query String
Canonical Headers
Signed Headers
Payload Hash先让后端为 PUT 生成一个 10 分钟 URL,并把 Content-Type: text/plain 纳入签名。然后做一正一反两次请求:
curl --fail-with-body -X PUT \
-H 'Content-Type: text/plain' \
--data-binary @hello.txt "$PRESIGNED_PUT_URL"
curl --fail-with-body -X PUT \
-H 'Content-Type: application/json' \
--data-binary @hello.txt "$PRESIGNED_PUT_URL"第一条应返回 2xx,第二条应稳定返回 403 或厂商对应的签名不匹配错误。若两条都成功,说明 Content-Type 没有进入签名约束,不能再声称“URL 已限制内容类型”。再把系统时间、代理和 query 编码加入排查轴:时钟漂移会改变签名时间窗口,企业代理改写 host/header/query 也会导致服务端计算出另一份 canonical request。
AWS 的 预签名 URL使用生成者身份的权限,CLI/SDK 使用 SigV4 时最长可配置 7 天,但临时凭证先过期时 URL 也随之失效;URL 本质上是 bearer token。OSS 推荐 V4,其 V4 预签名规则同样以 604800 秒为长期凭证上限,STS 生成的 URL 还受 STS 自身有效期约束。COS 不能复用 X-Amz-* 或 x-oss-* 参数,应由 COS SDK 生成预签名 URL,并使用 COS 自身的签名参数。
SDK 抽象只统一业务动作
一个可靠的 provider 接口统一的是业务语义,不是把三家构造参数压成同一个 S3Client。下面的 TypeScript 契约把对象身份、校验和失败语义固定下来,同时允许适配器保留厂商配置。
type Provider = "aws-s3" | "aliyun-oss" | "tencent-cos";
interface ObjectRef {
provider: Provider;
bucket: string;
key: string;
versionId?: string;
}
interface PutRequest {
key: string;
body: Uint8Array;
contentType: string;
checksumSha256: string;
metadata: Record<string, string>;
}
interface StoredObject extends ObjectRef {
size: number;
etag?: string;
checksumSha256?: string;
}
interface ObjectStore {
put(input: PutRequest): Promise<StoredObject>;
head(ref: ObjectRef): Promise<StoredObject | null>;
get(ref: ObjectRef): Promise<Uint8Array>;
delete(ref: ObjectRef): Promise<void>;
presignPut(input: Omit<PutRequest, "body">, ttlSeconds: number): Promise<string>;
}构造适配器时分别注入原生 SDK:AWS 适配器使用默认凭证链、region、可选 endpoint 和 forcePathStyle;OSS 适配器使用 region、endpoint、V4 signer 与 SecurityToken;COS 适配器使用 Region、完整 BucketName-APPID 和 BasicSessionCredentials。不要把 COS/OSS 当作 AWS SDK 的“换 endpoint 模式”,否则临时凭证字段、签名、错误码、multipart、校验和与域名差异会泄漏到业务层。
适配器还要统一四类失败,而不是统一厂商错误文本:
type StorageErrorCode =
| "NOT_FOUND"
| "ACCESS_DENIED"
| "INVALID_CONFIGURATION"
| "RETRYABLE";
class StorageError extends Error {
constructor(
readonly code: StorageErrorCode,
message: string,
readonly requestId?: string,
) {
super(message);
}
}404 只有在调用身份具备足够查询权限时才映射为 NOT_FOUND;显式拒绝映射为 ACCESS_DENIED;region、endpoint、bucket 格式和签名版本错误归入 INVALID_CONFIGURATION;超时、连接重置和可重试的 5xx 才进入有上限、带抖动的重试。PUT 是否可安全重试取决于 key 是否固定、body 是否可重放以及业务是否允许覆盖,不能只看 HTTP method。
跨云契约测试至少固定这些不变量:PUT 后 HEAD 的 size 与 SHA-256 符合预期;GET 摘要相同;删除后 HEAD 不再返回对象;错误 region 必须失败;过期或改写 header 的预签名必须失败;临时凭证不能列出其他 prefix。三家适配器分别跑真实开发桶,mock 只用于快速回归。
四种接入形态解决的不是同一个问题
只有一个云账号、对象不需要跨云迁移时,应用直接使用原生 SDK 通常最稳:签名、校验和、分片、加密和错误码都能完整表达,升级面也最小。业务代码仍保存结构化对象身份,但没有必要为了“将来可能换云”提前引入网络代理或伪装成 S3 的公共接口。
已经存在多云采购、租户驻留或可执行迁移计划时,provider 适配器更合适。适配器只统一 PUT、HEAD、GET、DELETE 和预签名等业务动作;版本、对象锁、归档恢复、事件通知和 KMS 等能力使用显式 capability,不能悄悄降级成最低公共子集。每次 SDK 或签名实现升级,都要对真实开发桶重跑契约测试。
集中网关适合统一审计、配额或遗留协议转换,但它会进入数据面:带宽、临时磁盘、连接数、超时和网关凭证都成为新的容量与故障边界。浏览器和大文件若本可直传云存储,不应仅为“统一 endpoint”让字节流绕过网关;网关更适合签发授权和记录对象状态,而不是默认转发全部对象内容。
桶复制适合异地容灾或读副本,不等于原子双写。复制通常异步,既有对象、删除标记、指定 version ID 的删除、生命周期动作、KMS 和桶级配置在不同平台还有不同传播规则;S3 复制行为、OSS 跨地域复制和 COS 复制行为都要求把这些动作分别判断。切换前必须验证“写入传播、删除传播、历史对象回填、加密对象和失败重放”。需要双活写入时,应先规定对象的唯一主 provider、不可变 key 和冲突裁决者,否则两个站点覆盖同一 key 后,没有一条跨厂商全序版本号能自动判定哪份是业务真相。
浏览器直传要有确认协议
服务端先认证业务用户,再生成不可猜、不可越权的 key,例如 dev/avatar/{tenantId}/{userId}/{uuid}.png。客户端只能获得该 key 的短期 PUT 授权;上传完成后,它提交的是 uploadId/key,而不是任意 URL。服务端用自己的身份 HEAD,校验 size、content-type、checksum、加密状态和 key 归属,通过后才把对象标记为可用。
CORS 只决定浏览器是否允许 JavaScript 读取响应,不替代对象存储鉴权。规则应精确到业务 origin、PUT/GET/HEAD 和必要 header;AllowedOrigin=* 加宽的是浏览器调用面,不能修复签名错误。预签名 PUT 若绑定了 Content-Type 或 checksum,浏览器请求必须逐字保持这些 header。
容量保护不能只写在前端。后端签发前检查租户额度和单对象上限,上传确认时再以 HEAD 结果复核。较大对象应使用 multipart:记录 upload ID、已完成 part、超时和 abort 责任;业务取消、凭证过期或进程崩溃后,未完成分片仍可能占用存储并产生费用,因此生命周期规则要清理 abandoned multipart uploads,应用也要在可控失败时主动 abort。
权限、加密与敏感数据一起设计
最小权限必须同时限制 action 和 resource。一个上传角色不应拥有整桶删除、修改 bucket policy、关闭日志或读取其他租户 prefix 的能力。AWS 的判断链可能同时包含 identity policy、bucket policy、SCP/RCP、VPC endpoint policy、Block Public Access 和 KMS key policy;OSS 是 RAM 角色、会话 policy、bucket policy/ACL 的共同结果;COS 是 CAM、bucket policy/ACL 与 STS policy 的共同结果。任何显式 Deny 都不能靠另一条 Allow 抵消。
临时凭证也不是可公开配置:
前端、移动端和 CI 不接收长期 AK/SK。token、预签名 URL、完整对象 key 不进入日志、埋点、截图、工单或异常上报。凭证 TTL 要覆盖上传重试窗口,但不能用“省得续签”为理由拉长到工作日级别。
服务端签发记录只保存主体、授权动作、key hash、过期时间和 request ID,不保存 secret。角色收窄后做反向验证:访问相邻 prefix 必须得到拒绝。
AWS S3 对新上传对象默认提供 SSE-S3 基线加密,见 S3 默认加密说明。切换 SSE-KMS 会增加 KMS 权限、region、调用配额和费用约束:上传者需要可使用 key,下载者也需要解密权限,跨账号还要检查 key policy。OSS KMS、COS KMS 同样应由各自 SDK 参数与密钥策略表达,不能用一个布尔 encrypted=true 隐藏密钥身份。
开发桶里的 Excel、附件、日志包和数据库导出仍可能含手机号、证件号、合同或令牌。测试数据先脱敏;日志只输出 provider、桶别名、key hash、size、request ID 和错误码;故障取证若必须保留对象样本,应进入受控证据桶并设置独立保留期。
回滚不是把 endpoint 改回去
对象存储切换至少有配置、写入、读取和数据四个状态。最稳妥的发布顺序是:
新 provider 先完成真实桶契约测试,业务仍只读写旧 provider。上线“读旧、影子写新”,为每次影子写记录源对象版本、目标对象身份和校验结果,对比 size、checksum、metadata 和错误率;影子写失败不改变用户结果,但必须进入可重放清单,不能只打一条日志。新对象主写新 provider,读取按对象记录中的 provider 路由,旧对象仍读旧 provider。
迁移历史对象并校验 checksum,成功后逐条更新对象记录,不能先批量改 URL。观察一个业务周期后停止旧写入,保留旧读和恢复窗口。
出现签名错误、错误率或费用异常时,先关闭新 provider 写流量,把新对象主写切回旧 provider;已经成功写入新云的对象继续按记录读取,不能通过全局 endpoint 强行指回旧云。切换期间的删除也要进入迁移日志:删除标记是否复制、指定 version ID 的永久删除是否传播、生命周期删除是否传播,都必须按实际平台规则验证,不能假设“开了复制就会同步删除”。影子对象按本次发布的独立 prefix 和迁移清单清理。只有确认没有业务记录引用后才删除,版本桶还要处理历史版本和 delete marker。
生命周期变更也采用相同思路:先导出当前规则,新增规则只命中测试 tag/prefix,放入少量带已知年龄的对象,观察转储或过期结果,再逐步扩大。回滚生命周期规则只能阻止未来动作,不能恢复已经删除的数据;恢复能力来自版本控制、复制、备份或合规保留,而不是“删掉规则”。
删除、版本和保留策略的真实语义
未启用版本控制时,DELETE 通常移除当前对象;启用后,普通 DELETE 可能只创建 delete marker,旧版本仍占容量。指定 version ID 的永久删除、Object Lock、retention 和 legal hold 又是另一组权限与合规动作。看到普通 LIST 为空,只能证明当前版本不可见,不能证明历史数据和费用已经消失。
误删防护的选择顺序取决于数据价值:可重建缓存用短生命周期和明确 prefix;业务附件启用版本并制定历史版本清理;不可篡改记录使用保留机制,但上线前必须验证账号、法务、容量和紧急处置责任。保留策略过宽会把“防误删”变成“无法合规删除且费用持续增长”。
从请求路径推导容量与费用
对象存储账单至少拆成六条:存储容量与存储类型、PUT/GET/LIST 等请求、外网或跨地域流量、低频/归档取回、KMS/日志等附加服务、版本与未完成 multipart 的隐性容量。价格数字会随地域、存储类型和活动变化,预算应从三家控制台的计价器和实际账单维度读取,不把固定单价写进配置。
容量估算先用业务变量:
月新增容量 = 日上传对象数 × 平均对象大小 × 30
稳态容量 ≈ 月新增容量 × 保留月数 × (1 + 历史版本系数)
月请求量 = PUT + multipart parts + HEAD + GET + LIST + DELETE
外网流量 = 客户端下载量 + 跨云复制量 + 回源量小文件场景可能是请求费用与元数据操作占主导,大文件下载则常被外网流量主导。把应用部署在对象存储同地域并确认走内网,通常能降低延迟和流量费用;但同云、同地域不代表 DNS 一定解析到内网,也不代表请求免费。通过 DNS 解析、链路监控和账单标签三处交叉确认。
告警不使用脱离负载的万能阈值。更可靠的判据是:每日新增容量偏离业务上传量;历史版本容量持续单调增长;未完成 multipart 数量超过正常上传窗口后不回落;GET/外网流量与下载业务指标失配;无 owner 的 prefix 或 bucket 出现费用。阈值由基线、预算和恢复目标决定。
让审计回答“谁对哪个对象做了什么”
应用日志只能证明业务发起过动作,云审计才能补上实际 API 调用者、时间、来源和 request ID。AWS CloudTrail trail 默认不记录 S3 data events,需要显式选择目标 bucket,并评估高请求量带来的事件费用,见 S3 CloudTrail data events。S3 server access logging 与 CloudTrail 的字段、延迟和用途不同,不能互相冒充。
OSS 需要结合 ActionTrail 与 OSS 访问日志,COS 结合操作审计与访问日志。无论平台,验收时都要用一次受控 PUT、GET、DELETE 反查主体、bucket/key、来源 IP、时间和 request ID,并确认日志桶不会与业务桶形成递归写日志。公开访问、bucket policy、CORS、生命周期、版本、KMS 和日志开关属于控制面变更,单独留审计记录和审批人。
长期治理可以落到一张资源登记表,但每个字段都要能被自动检查:provider、账号、region、bucket、数据分类、owner、prefix 规则、凭证来源、公开访问状态、CORS、版本与保留、生命周期、KMS key、审计去向、月预算和下线日期。每次 SDK 升级或兼容层改动,三家真实开发桶重跑正向 CRUD、错误 region、越权 prefix、预签名 header 错配和过期凭证用例。这样“兼容”才是持续验证出来的契约,而不是一次成功上传留下的幻觉。
