适合读者:想让桌面端、移动端、客户端程序上传/下载阿里云 OSS 文件,但又不想把 AccessKey Secret 暴露给用户的开发者。
1. 为什么需要预签名 URL?
上一篇讲的 OSS direct 模式适合后端服务,因为后端可以安全持有 AccessKey Secret。但如果你在客户端里写:
text
ALIYUN_ACCESS_KEY_SECRET=REDACTED_REAL_SECRET_DO_NOT_EMBED
哪怕你打包成二进制,用户或攻击者也可能逆向、抓包、读取配置文件,从而拿到长期密钥。上面这行是反例占位,真实项目里不要把任何真实 Secret 写进客户端。
预签名 URL 的思路是:
text
后端持有 AccessKey
-> 后端认证用户并生成短期 URL
-> 客户端只拿这些短期 URL 上传/下载
-> URL 过期后自动失效或需要重新申请
这样客户端不需要知道 AccessKey ID 和 AccessKey Secret。rusty-cat 在这个模式下只负责执行后端给出的上传/下载计划,不负责生成阿里云签名,也不会保存你的云密钥。
2. 适合哪些场景?
适合:
- 桌面端上传用户文件到 OSS。
- 移动端下载用户有权限访问的文件。
- 多租户系统,需要后端判断"谁能访问哪个对象"。
- 不希望客户端持有长期云密钥的场景。
- 后端已经有用户登录、权限校验和临时授权能力。
不适合:
- 没有后端、也不打算做 URL 签发服务的纯本地工具。
- URL 有效期极短但没有刷新机制的大文件传输。
- 后端和客户端对分片大小、part number、offset 无法保持一致的系统。
3. 依赖与 feature
toml
[dependencies]
rusty-cat = { version = "0.2.2", features = ["aliyun-oss-presigned"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
版本提示:本文按 GitHub 当前 provider 文档和本地示例整理。正式接入时请以 crates.io 和 docs.rs 对应版本为准:
aliyun-oss-presigned 会启用 provider-neutral 的预签名分片上传和 Range 下载能力。
4. 后端需要返回什么?
预签名模式不是"客户端随便传一个 URL 就完事"。后端需要提供一个明确的传输计划。
上传时,后端通常要返回:
| 字段 | 说明 |
|---|---|
upload_id |
OSS multipart upload ID,后端创建或管理。 |
total_size |
文件总大小,客户端和后端必须一致。 |
chunk_size |
分片大小,客户端必须按这个大小切片。 |
parts |
每个分片的 part_number、offset、size、presigned_url。 |
expires_at |
URL 过期时间,方便客户端判断是否需要刷新。 |
complete_endpoint |
可选,后端完成 multipart upload 的接口。 |
下载时,后端通常要返回:
| 字段 | 说明 |
|---|---|
download_url |
支持 Range GET 的短期下载 URL。 |
head_url |
可选。如果下载 URL 不支持 HEAD,可以提供单独 HEAD URL 或直接给 total_size。 |
total_size |
可选但推荐。已知大小时 SDK 可跳过 HEAD。 |
expires_at |
URL 过期时间。 |
headers |
可选业务 header。 |
注意:预签名 URL 本身也有访问权限,虽然短期有效,也不要写入公开日志或长期保存。
5. 预签名上传完整示例
下面示例用 PresignedMultipartUpload 执行阿里云 OSS 分片上传。示例里的 URL 全部是占位符,真实项目中应该由你的后端返回。
rust
use std::sync::Arc;
use rusty_cat::aliyun_oss_presigned as aliyun;
use rusty_cat::api::{
MeowClient, MeowConfig, PresignedMultipartUpload,
PresignedMultipartUploadPlan, UploadPounceBuilder,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let chunk_size = 1024 * 1024;
let total_size = 5 * chunk_size;
// 真实项目中,这些 URL 必须来自你的后端,不要写死在代码里。
// 这里用 REDACTED 表示签名参数已脱敏。
let urls = vec![
"https://oss.example.com/object?partNumber=1&uploadId=REDACTED&signature=REDACTED",
"https://oss.example.com/object?partNumber=2&uploadId=REDACTED&signature=REDACTED",
"https://oss.example.com/object?partNumber=3&uploadId=REDACTED&signature=REDACTED",
"https://oss.example.com/object?partNumber=4&uploadId=REDACTED&signature=REDACTED",
"https://oss.example.com/object?partNumber=5&uploadId=REDACTED&signature=REDACTED",
];
let parts = urls
.iter()
.enumerate()
.map(|(i, url)| {
aliyun::upload_part(
(i + 1) as u64,
i as u64 * chunk_size,
chunk_size,
*url,
)
})
.collect::<Vec<_>>();
let upload_protocol = PresignedMultipartUpload::new(
PresignedMultipartUploadPlan::new(total_size, chunk_size, parts)
.with_upload_id("backend-upload-id"),
);
let client = MeowClient::new(MeowConfig::default());
let task = UploadPounceBuilder::new(
"aliyun-presigned.bin",
"./aliyun-presigned.bin",
chunk_size,
)
.with_url(urls[0])
.with_breakpoint_upload(Arc::new(upload_protocol))
.build()?;
let outcome = client
.enqueue_and_wait(task, |record| {
println!(
"阿里云预签名上传进度:{:.2}% status={:?}",
record.progress() * 100.0,
record.status(),
);
})
.await?;
println!("上传完成:task_id={} payload={:?}", outcome.task_id, outcome.payload);
client.close().await?;
Ok(())
}
运行前必须替换:
urls:后端生成的真实 UploadPart 预签名 URL。total_size:真实文件大小。chunk_size:必须与后端签 URL 时使用的分片大小一致。./aliyun-presigned.bin:真实本地文件路径。
6. 上传参数逐个解释
| 参数或方法 | 说明 |
|---|---|
aliyun::upload_part(part_number, offset, size, url) |
把后端返回的单个分片 URL 转成 SDK 可识别的 part 描述。 |
part_number |
OSS 分片编号,通常从 1 开始。必须和后端生成 URL 时一致。 |
offset |
当前分片在本地文件中的起始字节位置。 |
size |
当前分片大小,最后一个分片可能小于 chunk_size。 |
PresignedMultipartUploadPlan::new(total_size, chunk_size, parts) |
创建完整上传计划。 |
with_upload_id("backend-upload-id") |
可选,用来记录后端或 OSS multipart upload ID。不要放敏感密钥。 |
PresignedMultipartUpload::new(plan) |
创建预签名上传协议对象。 |
with_breakpoint_upload(Arc::new(upload_protocol)) |
把预签名上传协议注入上传任务。 |
最容易出错的是 offset、size 和 chunk_size 不一致。后端按 1 MiB 签 URL,客户端却按 2 MiB 切片,就会把错误字节上传到错误 part。
7. 预签名下载完整示例
如果后端已经知道对象大小,推荐把 total_size 返回给客户端,这样 SDK 可以跳过 HEAD。
rust
use std::sync::Arc;
use rusty_cat::api::{
DownloadPounceBuilder, MeowClient, MeowConfig,
PresignedRangeDownload, PresignedRangeDownloadPlan,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// 真实项目中,这个 URL 来自后端;不要把真实签名 URL 写进文章或日志。
let download_url = "https://oss.example.com/object?signature=REDACTED";
let total_size = 5 * 1024 * 1024;
let download_protocol = PresignedRangeDownload::new(
PresignedRangeDownloadPlan::new(download_url)
.with_total_size(total_size),
);
let client = MeowClient::new(MeowConfig::default());
let task = DownloadPounceBuilder::new(
"aliyun-presigned.bin",
"./downloads/aliyun-presigned.bin",
1024 * 1024,
download_url,
)
.with_breakpoint_download(Arc::new(download_protocol))
.build();
let outcome = client
.enqueue_and_wait(task, |record| {
println!(
"阿里云预签名下载进度:{:.2}% status={:?}",
record.progress() * 100.0,
record.status(),
);
})
.await?;
println!("下载完成:task_id={}", outcome.task_id);
client.close().await?;
Ok(())
}
如果后端不给 total_size,下载准备阶段通常需要 HEAD。这要求预签名 URL 允许 HEAD,否则会失败。对新手来说,最稳妥的方式是让后端直接返回对象大小。
8. URL 过期与刷新
预签名 URL 最大的问题是有效期。过期太短,大文件还没传完就会 403;过期太长,URL 泄露后的风险更大。
建议:
- 小文件:URL 有效期可以短一些。
- 大文件:后端提供刷新接口,客户端在过期前重新获取 URL。
- 暂停很久后恢复:不要继续使用旧 URL,重新向后端申请新的传输计划。
- 日志里只打印 URL 的对象 key 或业务 ID,不打印完整 query string。
PresignedMultipartUploadPlan 支持刷新相关配置,实际生产是否启用刷新,取决于你的后端是否提供 URL refresh 能力。
9. 后端接口设计建议
上传初始化接口:
text
POST /api/files/presigned-upload/init
请求:
{
"file_name": "demo.bin",
"total_size": 5242880,
"chunk_size": 1048576
}
响应:
{
"upload_id": "业务上传ID",
"chunk_size": 1048576,
"total_size": 5242880,
"parts": [
{ "part_number": 1, "offset": 0, "size": 1048576, "url": "已脱敏的短期URL" }
],
"expires_at": "2026-05-17T12:30:00Z"
}
下载授权接口:
text
POST /api/files/presigned-download
响应:
{
"download_url": "已脱敏的短期URL",
"total_size": 5242880,
"expires_at": "2026-05-17T12:30:00Z"
}
后端要做的安全校验:
- 当前用户是否有权访问该对象。
- 文件大小和分片大小是否合理。
- URL 权限是否只覆盖目标对象和目标操作。
- URL 有效期是否足够短。
- complete multipart upload 前是否校验 part 列表。
10. 常见问题排查
| 现象 | 常见原因 | 处理建议 |
|---|---|---|
上传返回 403 |
URL 过期、HTTP method 不匹配、part number 不匹配。 | 重新生成 URL,检查后端签名参数。 |
| 上传完成但 OSS 看不到对象 | 后端没有 complete multipart upload。 | 增加完成接口或配置 completion request。 |
| 下载 prepare 失败 | URL 不允许 HEAD,也没有提供 total_size。 |
后端返回对象大小,或提供支持 HEAD 的 URL。 |
| 分片内容错乱 | 客户端 offset/size 与后端签名计划不一致。 | 后端和客户端统一 chunk_size 和 part 计算逻辑。 |
| 日志泄露 URL | 打印了完整预签名 URL。 | 日志脱敏,只保留业务文件 ID。 |
11. 本篇小结
阿里云 OSS 预签名模式的核心价值是:长期密钥留在后端,客户端只拿短期授权。
rusty-cat 在这个模式下负责执行传输计划,包括分片上传、Range 下载、进度回调、重试和生命周期控制。后端负责用户鉴权、URL 签发、URL 刷新和 multipart complete。
如果你的应用运行在用户机器上,预签名模式通常比 direct 模式更安全、更适合长期维护。