Rust 客户端安全上传下载微软 Azure Blob:rusty-cat SAS 预签名实战

适合读者:想让客户端上传/下载 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。
  • 分发进度回调。
  • 处理分片重试、暂停、恢复、取消。

核心类型包括:

  • PresignedMultipartUpload
  • PresignedMultipartUploadPlan
  • PresignedRangeDownload
  • PresignedRangeDownloadPlan
  • rusty_cat::azure_blob_sas helper 函数

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 模式更安全、更容易长期维护。

相关推荐
砚凝霜4 分钟前
【软考信息安全】第十三章 网络安全漏洞分类管理与扫描处置
安全·web安全
羿悦科技37 分钟前
自动侧推定位机构中的接近开关:让工件靠边更准确
安全·智慧城市
Xudde.1 小时前
CVE-2017-9993漏洞复现
笔记·学习·安全·web安全
杭州领祺科技1 小时前
036 绿电直连项目二次系统通信与安全架构选型:650/688/618 三文 + 27 号令 4 区 7 产品
安全·安全架构·储能·电力·绿电直连
w2sfot3 小时前
【JS加密在线】jsjiami.online(让JavaScript 更安全!)
开发语言·javascript·安全
墨云安全3 小时前
红队评估和渗透测试的区别是什么?2026 年从目标、方法到交付物的对比
网络·安全·web安全·网络安全·渗透测试
yu_zhidun3 小时前
终端资产管理平台实战配置:从空白环境到动态资产台账落地
安全·域智盾
Solis程序员4 小时前
Stripe 幂等表:把「不确定的重试」变成「查表返回」
前端·安全·bootstrap·php·agent
陈然信息站5 小时前
皮尔磁纸板进料安全方案选型指南:O300传感器与myPNOZ、PNOZmulti 2对比
大数据·安全·业界资讯
飞跨浏览器6 小时前
亚马逊 Passkey 怎么配置?在飞跨浏览器内完成设置
安全