对象存储 SDK、临时 URL 与权限:对象访问怎样被委托且可撤销
对象存储用 bucket 和 key 定位字节。应用则通过业务文件 ID 记录文件属于谁、用于什么、当前能否访问。选择怎样映射这两套标识时,需要一并考虑下载权限、旧链接失效,以及重试时的新建版本或覆盖行为。
SDK 负责把这些存储操作变成经过签名的 HTTP 请求。预签名 URL 则把一个限定的存储请求交给其他客户端执行,文件数据可以直接在客户端与存储之间传输。
对象身份和业务文件身份怎样对应
bucket、key、versionId 各自表示什么
业务文件 fileId
├─ 所有者、租户、业务引用、审核状态、保留规则
└─ 当前内容定位
├─ provider / endpoint 请求发送到哪个存储系统
├─ bucket 容器或命名空间
├─ key 对象完整名称,例如 tenant-a/files/随机ID/source
├─ versionId 启用且支持版本化时的指定版本
├─ length / checksum 长度与内容校验信息
└─ media type / metadata 对象属性与应用附加信息key 中的斜杠通常用于构造前缀。控制台把前缀显示为文件夹,不意味着存储内部存在普通文件系统的目录、父目录权限或原子目录移动。对大量对象执行“移动目录”,往往需要逐个复制、验证、删除,并处理部分失败。S3 对象 key 说明
把原始文件名直接作为 key 会遇到名称冲突、特殊字符、路径误解和信息泄漏。常见设计是用服务生成的随机 ID 作为存储名,把用户展示名另存为元数据。扩展名可以保留为提示,但授权不依赖名称的可猜测程度。
versionId 来自提供方的版本化机制。未启用版本化时,同 key 的 PUT 通常替换当前对象;启用后,当前版本指针与历史版本删除又是两类操作。不可变 key 则由应用每次生成新名称,再更新业务映射,两种方式可以组合使用。
ETag 是服务返回的实体标记,是否等于 MD5 取决于上传方式、加密及服务实现。使用 checksum 时,还要指定算法与校验范围。用户 metadata 则保存应用提供的声明:仅写入一个 sha256 字符串,服务不会据此自动验证整个对象,验证需要另行执行。
为什么业务接口不直接接收任意 key
允许用户提交 bucket 和 key,再用服务端高权限凭证执行 GET,会把应用变成越权读取代理。更稳妥的请求是“下载 fileId”,应用根据登录身份查询可访问文件,再从数据库取得受控的 bucket、key 和版本。
这也便于替换存储系统。业务引用指向稳定的 fileId;迁移程序可以把内容复制到另一个桶、核对摘要后更新定位,而不必修改每一条业务记录中的 URL。
对象存储适合大量独立对象、弹性容量和 HTTP 访问。需要随机覆盖文件中少量字节、普通文件锁或频繁原子重命名时,要重新评估文件系统或块存储。S3 的对象、存储类别、版本化和访问控制基本模型可从 S3 用户指南查阅。
用 SDK 完成上传、下载和错误识别
第一次成功请求需要哪些配置
下载 S3 实验工程,解压并进入 file-s3-lab。需要 Linux、Bash、Docker Engine、Compose v2、openssl 和 unzip,运行用户拥有 Docker 权限。Garage 2.2.0 服务仅监听宿主回环 18390,应用使用 AWS SDK for Java 2.54.13;构建与应用容器使用宿主 UID/GID。
umask 077
mkdir -p .m2
docker run --rm --user "$(id -u):$(id -g)" --entrypoint mvn \
-e MAVEN_CONFIG=/m2 -v "$PWD:/work" -v "$PWD/.m2:/m2" -w /work \
maven:3.9.12-eclipse-temurin-17 \
-B -Dmaven.repo.local=/m2 clean package
bash setup.sh
bash run.sh object worksetup.sh 负责单节点布局、file-lab 桶和仅该桶读写的密钥。正常运行会输出:
object: file upload/download exact; delete HEAD=404这条命令通过 RequestBody.fromFile 上传真实文件,使用 ResponseTransformer.toFile 下载到新路径,比较字节后删除对象,并要求后续 HEAD 返回 404。它同时验证了地址、凭证、桶权限与数据路径。
如果已经运行过分片与断点续传实验,继续使用原目录即可,不必再次 setup。已有服务停止后可通过 docker compose --env-file work/garage.env -p fs21 up -d 启动。RPC secret 与访问密钥保存在 work 下,不能作为文章附件或提交内容。
endpoint、region 与地址形式
客户端的关键配置如下;这里省略导入语句,完整构建依赖与源码在实验工程中:
S3Client client = S3Client.builder()
.region(Region.of("garage"))
.endpointOverride(URI.create("http://127.0.0.1:18390"))
.credentialsProvider(EnvironmentVariableCredentialsProvider.create())
.forcePathStyle(true)
.build();endpoint 确定连接目标,region 参与签名和请求路由。S3 兼容服务经常使用自定义 endpoint 和约定 region;只改 endpoint 却保留错误 region,可能建立了连接却无法通过签名验证。
两种常见地址形式为:
path-style https://storage.example.com/bucket-a/object-key
virtual-hosted https://bucket-a.storage.example.com/object-key前一种把桶名放在路径,后一种放在 Host。DNS、TLS 证书与反向代理都要支持所选形式。预签名后再改 Host、路径编码或 endpoint,会改变参与签名的请求。不要先对内网地址签名,再把 URL 中的主机名简单替换成公网域名。
实验采用 HTTP 回环地址,避免要求读者配置本地域名和证书;生产接入使用正确证书的 HTTPS。遇到证书错误应检查信任链和域名,不用跳过证书校验作为常规修复。
Garage 不完整实现 AWS IAM policy、版本化或 KMS。实验中 SDK 默认附加 checksum 的载荷组合会得到 400 Invalid payload signature,所以 run.sh 仅在这个本地组合上设置请求/响应 checksum 的 WHEN_REQUIRED,并用实际字节比较核对结果。AWS S3 应使用其适用的默认完整性保护。Garage S3 兼容性、AWS SDK 数据完整性设置
凭证、连接与重试属于客户端生命周期
实验显式使用环境变量凭证,便于限定测试身份。生产服务优先使用工作负载身份或短期凭证,例如实例、容器或角色凭证,由 SDK 的凭证提供者按其机制刷新。默认凭证链会按顺序检查多个来源;排查“为何用了错误账号”时要核对实际来源,不能只看代码里有没有写 access key。Java SDK 默认凭证链
一个服务通常复用线程安全的 SDK 客户端及其连接资源,在应用停止时关闭。逐请求创建客户端会反复建立资源;未关闭的下载流长期占着连接,又可能耗尽连接池。依赖注入配置需要明确共享 HTTP 客户端的创建者和关闭时机。
需要分别设置连接建立超时、读写超时、单次尝试时间和包含重试的总调用时间。若总调用限制比一次慢下载所需时间还短,SDK 会中断一个本来能够完成的传输;若没有总限制,重试又可能超过用户请求或任务租约的期限。具体参数依所选同步、异步 HTTP 客户端而异。Java SDK HTTP 配置
上传请求体必须能够重放。文件路径可在重试时重新打开;只读一次的输入流则需要可靠的重开方式,或转为分片并保留未确认的源区间。已知长度必须准确,声明太短可能截断,太长可能造成等待或失败;同步 API 为未知长度计算内容长度时可能缓冲整个输入。Java SDK S3 上传实践
同步 S3Client 适合有界工作线程上的直接调用。S3AsyncClient 和 Transfer Manager 可管理异步请求、并行传输及相关调度,但仍要约束并发、队列和可重放数据,不能把线程释放理解为内存、连接和带宽也已释放。大文件分片的具体恢复见前一篇。
预签名 URL 把哪一项请求交给客户端
签名绑定的方法、地址和请求头
签发过程通常不需要向 S3 发出网络请求。应用检查业务权限后,使用可用凭证在本地计算签名,再把 URL 交给客户端。真正的存储权限判断发生在客户端执行请求时,因此“成功生成 URL”无法验证桶是否存在或签发身份是否有该操作权限。Java SDK 预签名示例
| 要素 | 签发时的含义 | 客户端需要保持的条件 |
|---|---|---|
| HTTP 方法 | GET、PUT 等具体操作 | 不能拿 GET 地址执行 PUT |
| bucket、key、版本参数 | 请求的精确对象 | 不自行拼接或改写路径、查询参数 |
| Host、region | 目标及签名上下文 | 代理转发后仍满足签名要求 |
| 已签名请求头 | Content-Type、校验和等参与签名的值 | 按签发结果发送,不能随意变更 |
| 签名时间和有效期 | 允许发起请求的时间条件 | 客户端与服务时钟合理同步 |
| 临时凭证令牌 | 短期身份的组成部分 | URL 有效期还受凭证本身到期限制 |
签名覆盖哪些头,要看预签名结果中的 signed headers。一个没有签入的普通头,不能仅因它出现在最初的业务表单中就被当作受保护的条件。尤其要区分以下限制:签名 Content-Type 约束的是请求声明;实际文件格式要解析检查。签名校验和可在服务支持的情况下约束内容;它不会自动产生业务文件大小范围。需要 content-length-range 等表单策略时,应使用提供方支持的预签名 POST policy 并核对其条件,不能给任意 PUT URL 添加一个前端变量就认为服务会强制执行。
验证重复 PUT、改头和改参数
在前面的实验目录运行:
bash run.sh presign work
bash run.sh signed-get workpresign 使用五分钟有效期,生成要求 Content-Type 为 text/plain 的 PUT 请求,执行以下真实操作:第一次上传一个字节,再用同一 URL 上传另一个字节,GET 核对对象已更新;改变 Content-Type 后要求 403,并确认对象仍保留上一次合法内容;修改签名 URL 的过期参数后要求签名拒绝。
关键输出为:
presign: reusable PUT replaced bytes; changed Content-Type=403 AccessDenied (Invalid signature); download.url saved locally
signed GET: status=200Garage 2.2 的错误码是 AccessDenied,消息包含 Invalid signature;不要把它写成 AWS 一定返回的错误码。实验同时检查状态、错误内容和对象未被修改,避免把服务不可达当作签名校验成功。
完整 GET URL 保存到 work/download.url,不打印在日志中。在同一 Linux 宿主可以用 curl 下载。使用变量时不要开启 shell 的命令跟踪;共享机器还要注意命令行参数可能被其他进程观察:
SIGNED_URL=$(<work/download.url)
curl -q --noproxy '*' --fail-with-body --silent --show-error \
"$SIGNED_URL" -o work/signed.bin
test "$(wc -c < work/signed.bin)" -eq 1 || exit 1
unset SIGNED_URLURL 到期后重新运行 presign,再执行下载。保持原始查询参数,不要把浏览器显示的转义形式随意重新编码。
预签名 URL 在允许时间内可以重复使用。签发一个 PUT URL 后,即使业务已经把第一次上传标记为审核通过,旧 URL 仍可能覆盖同 key 内容。常见处理是向独占的暂存 key 上传,校验后将已确认内容放到另一个不可变 key,再让业务引用它;或者采用服务支持的版本与条件写入,并逐项验证兼容实现。S3 预签名 URL 行为、S3 条件写入
有效期、一次性和撤销是不同设计
有效期限制请求何时可被接受。AWS S3 在请求开始时检查过期条件,已经开始的下载可以继续;中断后重新连接又是一个新请求。临时凭证提前过期,也会缩短 URL 的实际可用时间。S3 预签名 URL 行为
“一次性下载”需要额外的消费记录和受控入口。若应用在首次请求时消费令牌,再返回一个仍可重复使用的 S3 URL,客户端依然能复用那个 URL。可根据风险选择应用代理传输、短时签名、动态授权网关或特定 CDN 的访问机制,同时说明用户拿到字节之后无法撤回其本地副本。
撤销方案的影响范围也不同:删除目标会影响引用该对象的全部链接;禁用签发密钥影响该身份的其他请求;修改桶策略或撤销读权限影响匹配规则的所有对象。业务登录态退出只改变应用会话,存储侧并不知道这个动作。
可以在本地验证权限变化对旧 URL 的作用,先重新生成未过期的 URL,再执行:
bash run.sh presign work
docker compose --env-file work/garage.env -p fs21 exec -T storage \
/garage bucket deny --read --key fs21-lab file-lab
if ! bash run.sh signed-denied work; then
docker compose --env-file work/garage.env -p fs21 exec -T storage \
/garage bucket allow --read --key fs21-lab file-lab
exit 1
fi
docker compose --env-file work/garage.env -p fs21 exec -T storage \
/garage bucket allow --read --key fs21-lab file-lab
bash run.sh signed-get work预期依次看到 403 和恢复后的 200。实验撤销的是 Garage 桶级读取权限,不是实现 AWS IAM 的精细条件,也不是单个 URL 的一次性撤销。若恢复验证前 URL 已经过期,重新签发再验证,避免把时间变化混进权限对照。
权限部署、浏览器访问与故障处理
先划分管理权限和数据权限
建桶、改策略、配置生命周期属于管理操作;读取、写入和删除对象属于数据操作。运行应用通常只需要指定桶和前缀上的部分数据权限。删除任务、上传签发服务、只读下载服务可以使用不同身份,降低误操作范围。
在 AWS 上,ListBucket 的资源是桶,GetObject/PutObject 的资源是对象 ARN,前缀条件和显式拒绝规则需要按 action 分别设计;使用 SSE-KMS 还要满足相关 KMS 权限。不能把本地 Garage 的桶级 RW 配置复制成 AWS policy 并宣称等价。按实际 action、resource、condition 查询 AWS 桶策略示例,先在专用测试桶验证允许与拒绝,再应用到生产。
上传接口还要限制用户能申请多少未完成会话、累计容量和有效期。数据库中记录 fileId、申请者、目标 key 与完成状态,避免用户长期囤积签名 URL 绕过业务配额。
CORS 只管理浏览器跨源读取
curl 能成功,浏览器却报 CORS 错误时,先在 F12 的 Network 找预检 OPTIONS 和真正的 PUT/GET。检查浏览器 Origin、Access-Control-Request-Method、Access-Control-Request-Headers,再对照桶的允许来源、方法、头以及需要暴露给 JavaScript 的 ETag 等响应头。S3 CORS 配置
浏览器不允许 JavaScript 读取响应,不表示服务一定没有执行写入。检查真实请求是否已发送、对象是否已存在,再决定重试。CORS 也不会限制 curl 或其他服务器客户端;没有存储鉴权的公开对象,无法靠只允许某个 Origin 变成私有对象。
引入 CDN 后,还要处理另一份缓存:鉴权在源站还是边缘执行,缓存键是否包含必要参数,私有响应是否可能被不同用户复用,源对象删除后缓存何时失效。预签名参数应避免进入访问日志和分析平台,页面的 Referrer-Policy 也应防止带签名的地址外泄。
按服务结果定位,而不是只看 SDK 异常类名
| 表现 | 需要读取的信息 | 处理方向 |
|---|---|---|
| 连接超时、域名错误 | endpoint、DNS、路由、代理、TLS | 先恢复连接;此时可能尚未进入存储鉴权 |
| 403 AccessDenied | action、bucket/key、签发身份、策略及有效期 | 核对真实权限;不要通过换成管理员凭证长期绕过 |
| 签名不匹配 | 方法、Host、region、编码、时间、已签名头 | 比较签发条件和实际请求,避免代理改写 |
| 404 NoSuchKey | 完整 key、大小写、版本与是否已完成上传 | 从业务映射查定位,不能把所有 404 都视为可以删除记录 |
| 429、503、SlowDown | 提供方错误码、请求 ID、并发和重试次数 | 有界退避,降低并发,检查热点与服务配额 |
| 上传超时、结果不明 | 请求是否可能到达、固定 key 的内容 | 查对象与会话后恢复,避免盲目覆盖 |
| 下载连接不断占用 | 响应流是否关闭、是否全量消费、HTTP 客户端配置 | 修正资源所有权,观察连接池与调用队列 |
ListObjectsV2 之类的列表操作要遍历 continuation token。分页列表适合枚举对象,不适合作为业务事务中“文件一定存在且永远不会改变”的锁。高频存在性查询应优先使用已知 key 的 HEAD,并结合应用记录的版本和状态。
桶当前占用容量只是费用的一部分。列表、HEAD、下载出口、跨地域访问、未完成分片、历史版本及存储类别取回都可能产生开销。按业务功能统计字节量和请求次数,可以判断应该优化下载缓存、批处理请求,还是清理过期内容。
实验结束后执行 docker compose --env-file work/garage.env -p fs21 down 停止服务。work 与 storage 仍保留,其中的密钥和签名 URL 继续按敏感文件管理;不再需要恢复环境或重现请求后,再清理本次实验文件。
