Rust 客户端安全上传下载阿里云 OSS:rusty-cat 预签名 URL 实战

适合读者:想让桌面端、移动端、客户端程序上传/下载阿里云 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.iodocs.rs 对应版本为准:

aliyun-oss-presigned 会启用 provider-neutral 的预签名分片上传和 Range 下载能力。

4. 后端需要返回什么?

预签名模式不是"客户端随便传一个 URL 就完事"。后端需要提供一个明确的传输计划。

上传时,后端通常要返回:

字段 说明
upload_id OSS multipart upload ID,后端创建或管理。
total_size 文件总大小,客户端和后端必须一致。
chunk_size 分片大小,客户端必须按这个大小切片。
parts 每个分片的 part_numberoffsetsizepresigned_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)) 把预签名上传协议注入上传任务。

最容易出错的是 offsetsizechunk_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 模式更安全、更适合长期维护。

相关推荐
灵机一物1 小时前
灵机一物AI原生电商小程序、PC端(已上线)-【技术深度解析】Bun 6 天 AI 重写 96 万行代码:从 Zig 迁移 Rust 全流程与行业影响
开发语言·人工智能·rust
Arman_1 小时前
03 rusty-cat 进阶解析:架构设计、云存储接入、安全模型与长期维护评估
css·安全·rust·文件分片上传·文件分片下载
黎阳之光1 小时前
黎阳之光|实验室全域实景管控,一屏掌控安全态势
安全
闵孚龙1 小时前
Claude Code 沙箱系统全解析:Seatbelt、Bubblewrap、AI Agent 安全隔离、权限治理与企业级防护
人工智能·安全
techdashen2 小时前
半小时读懂 Rust:从语法符号到所有权思维
开发语言·rust
晓梦林2 小时前
HiddenGate靶场学习笔记
笔记·安全·web安全
闵孚龙2 小时前
Claude Code CLAUDE.md 用户指令覆盖层全解析:AI Agent 记忆系统、上下文工程、规则分层、团队协作与安全治理
人工智能·安全
程序猿编码2 小时前
并发SSH口令审计器:多进程协作的安全检测工具设计与原理(C/C++代码实现)
c语言·安全·ssh
YIN_尹2 小时前
关于论文《FLUSH+RELOAD:一种高分辨率、低噪声的L3缓存侧信道攻击》的理解
安全·缓存·系统安全·缓存侧信道攻击