适合读者:想让客户端上传/下载 Azure Blob 文件,但不想把 Storage Account Key 暴露到客户端的 Rust 开发者。
1. SAS 是什么?为什么比直连更适合客户端?
Azure SAS 的全称是 Shared Access Signature,可以理解为一段带权限范围和过期时间的临时访问授权。
直连模式是:
text
客户端或服务进程持有 Account Key
-> 本地给 Azure 请求签名
SAS 模式是:
text
后端持有 Account Key 或其他云端授权能力
-> 后端生成短期 SAS URL
-> 客户端只拿 SAS URL 上传/下载
SAS 更适合客户端,因为:
- 客户端不需要知道 Storage Account Key。
- SAS 可以限制到某个 container/blob。
- SAS 可以限制权限,例如只读、只写、只创建。
- SAS 可以设置过期时间。
- 泄露后的影响范围比长期 Account Key 小得多。
本文不会展示真实 SAS token。所有 URL 中的 sig、sv 等参数都用 REDACTED 表示。
2. rusty-cat 在 Azure SAS 模式里做什么?
Azure SAS 模式中,rusty-cat 不负责生成 SAS,也不接触 Account Key。它负责:
- 根据后端给出的 SAS URL 上传 block。
- 生成或使用 block id。
- 通过 Put Block 上传分片。
- 通过 Put Block List 提交最终 Blob。
- 使用 SAS URL 做 Range 下载。
- 根据
total_size跳过或执行HEAD。 - 分发进度回调。
- 处理分片重试、暂停、恢复、取消。
核心类型包括:
PresignedMultipartUploadPresignedMultipartUploadPlanPresignedRangeDownloadPresignedRangeDownloadPlanrusty_cat::azure_blob_sashelper 函数
3. 依赖与 feature
toml
[dependencies]
rusty-cat = { version = "0.2.2", features = ["azure-blob-sas"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
版本提示:正式接入前,请以 crates.io 和 docs.rs 的当前版本为准:
azure-blob-sas 会启用 provider-neutral presigned 能力和 Azure SAS helper。
4. 后端需要返回什么?
上传时,后端至少要返回:
| 字段 | 说明 |
|---|---|
blob_sas_url |
目标 Blob 的 SAS URL。需要允许写入 block 和提交 block list。 |
total_size |
文件总大小。 |
chunk_size |
分片大小。 |
expires_at |
SAS 过期时间。 |
business_upload_id |
可选,业务上传 ID,方便恢复和审计。 |
下载时,后端至少要返回:
| 字段 | 说明 |
|---|---|
download_sas_url |
允许读取目标 Blob 的 SAS URL。 |
total_size |
推荐返回。返回后 SDK 可以跳过 HEAD。 |
expires_at |
SAS 过期时间。 |
headers |
可选,业务需要的额外 header。 |
SAS 权限建议:
| 操作 | 建议权限 |
|---|---|
| 上传新 Blob | write (w) 和 create (c)。 |
| 覆盖或提交 block list | 根据后端策略确保可以 Put Block / Put Block List。 |
| 下载 | read (r)。 |
| 获取大小 | 如果不提供 total_size,URL 或额外授权需要支持 HEAD。 |
5. Azure SAS 上传完整示例
下面示例演示如何用一个 Blob SAS URL 生成每个 block 的上传 URL,并在所有 block 完成后提交 block list。
rust
use std::sync::Arc;
use rusty_cat::api::{
MeowClient, MeowConfig, PresignedMultipartUpload,
PresignedMultipartUploadPlan, UploadPounceBuilder,
};
use rusty_cat::azure_blob_sas as azure;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// 真实项目中,这个 URL 必须由后端返回。
// 这里的 REDACTED 代表签名参数已脱敏,不能直接运行。
let blob_sas_url =
"https://account.blob.core.windows.net/container/blob.bin?sv=REDACTED&sig=REDACTED";
let chunk_size = 1024 * 1024;
let total_size = 5 * chunk_size;
let part_count = 5;
let parts = (0..part_count)
.map(|i| {
azure::put_block_from_blob_url(
blob_sas_url,
i,
i as u64 * chunk_size,
chunk_size,
)
})
.collect::<Result<Vec<_>, _>>()?;
let block_ids = (0..part_count)
.map(azure::block_id_by_index)
.collect::<Vec<_>>();
let block_id_refs = block_ids
.iter()
.map(String::as_str)
.collect::<Vec<_>>();
let complete_request = azure::put_block_list_request(
blob_sas_url,
block_id_refs,
)?;
let upload_protocol = PresignedMultipartUpload::new(
PresignedMultipartUploadPlan::new(total_size, chunk_size, parts)
.with_complete_request(complete_request),
);
let client = MeowClient::new(MeowConfig::default());
let task = UploadPounceBuilder::new(
"azure-sas.bin",
"./azure-sas.bin",
chunk_size,
)
.with_url(blob_sas_url)
.with_breakpoint_upload(Arc::new(upload_protocol))
.build()?;
let outcome = client
.enqueue_and_wait(task, |record| {
println!(
"Azure SAS 上传进度:{:.2}% status={:?}",
record.progress() * 100.0,
record.status(),
);
})
.await?;
println!("上传完成:task_id={} payload={:?}", outcome.task_id, outcome.payload);
client.close().await?;
Ok(())
}
运行前必须替换:
blob_sas_url:后端返回的真实 SAS URL。total_size:真实文件大小。chunk_size:与后端规划一致的分片大小。part_count:按文件大小和分片大小计算,不要写死。./azure-sas.bin:真实本地文件路径。
6. 上传参数逐个解释
| 参数或方法 | 说明 |
|---|---|
azure::put_block_from_blob_url(blob_sas_url, index, offset, size) |
根据 Blob SAS URL 生成某个 block 的 Put Block 请求描述。 |
index |
block 序号,用来生成 block id 和 URL 参数。 |
offset |
该 block 对应本地文件起始字节位置。 |
size |
该 block 大小,最后一个 block 可能小于 chunk_size。 |
azure::block_id_by_index(index) |
根据序号生成稳定 block id。 |
azure::put_block_list_request(blob_sas_url, block_id_refs) |
创建提交 block list 的完成请求。 |
with_complete_request(complete_request) |
告诉 SDK 上传完所有 block 后提交 block list。 |
with_breakpoint_upload(...) |
把 SAS 上传协议注入上传任务。 |
最关键的点是:**上传 block 的 block id 列表必须和最终 Put Block List 使用的 block id 完全一致。**否则 Azure 可能找不到 block,或者提交出错误内容。
7. Azure SAS 下载完整示例
如果后端知道 Blob 大小,推荐直接返回 total_size:
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 来自后端。不要把完整 SAS 写入日志。
let download_sas_url =
"https://account.blob.core.windows.net/container/blob.bin?sv=REDACTED&sig=REDACTED";
let total_size = 5 * 1024 * 1024;
let download_protocol = PresignedRangeDownload::new(
PresignedRangeDownloadPlan::new(download_sas_url)
.with_total_size(total_size),
);
let client = MeowClient::new(MeowConfig::default());
let task = DownloadPounceBuilder::new(
"azure-sas.bin",
"./downloads/azure-sas.bin",
1024 * 1024,
download_sas_url,
)
.with_breakpoint_download(Arc::new(download_protocol))
.build();
let outcome = client
.enqueue_and_wait(task, |record| {
println!(
"Azure SAS 下载进度:{:.2}% status={:?}",
record.progress() * 100.0,
record.status(),
);
})
.await?;
println!("下载完成:task_id={}", outcome.task_id);
client.close().await?;
Ok(())
}
如果没有 with_total_size(total_size),SDK 可能需要通过 HEAD 获取大小。这要求 SAS URL 或后端提供的 head URL 支持 HEAD。对小白来说,最稳妥的方式是让后端直接返回文件大小。
8. SAS URL 过期怎么处理?
SAS URL 是短期授权,过期后常见表现是 403。
建议:
- 后端返回
expires_at。 - 客户端记录任务开始时间和预计剩余时间。
- 大文件上传时,在过期前请求新 SAS。
- 暂停很久后恢复时,不要继续用旧 SAS。
- 下载失败如果是过期导致,重新向后端申请下载 SAS,再重建任务。
日志里不要打印完整 SAS URL。可以这样记录:
text
file_id=业务文件ID provider=azure-sas status=expired
不要这样记录:
text
download_url=https://account.blob.core.windows.net/...?...&sig=REDACTED_REAL_SIGNATURE
9. 后端接口设计建议
上传授权接口:
text
POST /api/azure/blob/sas-upload
请求:
{
"file_name": "demo.bin",
"total_size": 5242880,
"chunk_size": 1048576
}
响应:
{
"blob_sas_url": "已脱敏的 SAS URL",
"total_size": 5242880,
"chunk_size": 1048576,
"expires_at": "2026-05-17T12:30:00Z",
"business_upload_id": "upload-001"
}
下载授权接口:
text
POST /api/azure/blob/sas-download
响应:
{
"download_sas_url": "已脱敏的 SAS URL",
"total_size": 5242880,
"expires_at": "2026-05-17T12:30:00Z"
}
后端必须做:
- 用户鉴权。
- Blob 路径权限校验。
- SAS 权限最小化。
- SAS 过期时间控制。
- 审计记录,但不要记录完整
sig。 - 必要时提供刷新 SAS 的接口。
10. 与 direct 模式对比
| 维度 | direct | SAS |
|---|---|---|
| 客户端是否持有 Account Key | 是 | 否 |
| 是否适合公开客户端 | 不适合 | 更适合 |
| 后端复杂度 | 低 | 中等,需要签发 URL |
| 权限范围 | 取决于 Account Key 权限 | 可限制到某个 Blob 和某些操作 |
| 过期机制 | Account Key 通常长期有效 | SAS 可短期有效 |
| 泄露风险 | 高 | 相对低,但仍需保护 URL |
一句话建议:
后端服务可以 direct,用户侧客户端优先 SAS。
11. 常见问题排查
| 现象 | 常见原因 | 处理建议 |
|---|---|---|
403 |
SAS 过期、权限不足、URL 被错误修改。 | 重新签发 SAS,检查权限和过期时间。 |
| block 上传成功但 Blob 不存在 | 没有提交 Put Block List。 | 确认 with_complete_request(...) 已设置。 |
| 提交 block list 失败 | block id 列表不一致或顺序错误。 | 确保 block_id_by_index 和 completion 使用同一批 block id。 |
| 下载 prepare 失败 | SAS 不允许 HEAD,且没有提供 total_size。 |
后端返回 total_size。 |
| Range 下载失败 | SAS 没有读权限或 Blob 不存在。 | 检查 SAS 权限和 Blob 路径。 |
| 日志泄露签名 | 打印了完整 SAS URL。 | 做 URL 脱敏,只记录业务 ID。 |
12. 本篇小结
Azure Blob SAS 模式的核心价值是安全边界清晰:Account Key 留在后端,客户端只拿短期、有限权限的 URL。
rusty-cat 负责执行上传下载任务,包括分片、重试、进度、完成请求和 Range 下载;你的后端负责用户鉴权、SAS 签发、刷新和审计。
如果你要做面向用户的 Rust 桌面端、CLI 或客户端文件传输,SAS 模式通常比 direct 模式更安全、更容易长期维护。