Skip to main content

tdm_server_rust/utils/
cos_sts.rs

1//! 腾讯云 COS STS 临时密钥
2//!
3//! 基于 `cos-rust-sdk` 的 STS (Security Token Service) 获取临时访问凭证。
4//! 对齐 Java `CosStsClient`。
5//!
6//! ## 使用场景
7//!
8//! 前端直传 COS 时,服务端下发 STS 临时密钥和策略,
9//! 前端用临时密钥直接 PUT 文件到 COS,避免暴露主密钥。
10//!
11//! ## 安全模型
12//!
13//! - 临时密钥有效期由 `duration_seconds` 控制
14//! - 策略限定单对象写入(`PutObject`)或单对象读取(`GetObject`)
15//! - 文件大小通过 `content-length` 条件限制
16
17use cos_rust_sdk::sts::{GetCredentialsRequest, Policy, Statement, StsClient};
18use std::collections::HashMap;
19
20/// STS 临时凭证
21///
22/// 包含临时 SecretId、SecretKey 和 SessionToken,
23/// 前端用于签署 COS 请求。
24#[derive(Debug, Clone)]
25pub struct TempCredentials {
26    /// 临时 SecretId
27    pub tmp_secret_id: String,
28    /// 临时 SecretKey
29    pub tmp_secret_key: String,
30    /// 会话 Token(上传时需在 Header 中携带 `x-cos-security-token`)
31    pub session_token: String,
32}
33
34/// 从 bucket 名解析 appId
35///
36/// COS bucket 命名格式为 `{name}-{appid}`,如 `image-dev-1317356496`。
37///
38/// # 参数
39///
40/// - `bucket`: 完整的 bucket 名称(含 appId 后缀)
41///
42/// # 返回值
43///
44/// 返回 appId 字符串(`-` 之后的部分)。
45///
46/// # Errors
47///
48/// 当 bucket 名不包含 `-` 时返回 `AppError::Oss`。
49pub fn extract_app_id(bucket: &str) -> crate::error::ApiResult<String> {
50    bucket
51        .rsplit_once('-')
52        .map(|(_, app_id)| app_id.to_string())
53        .ok_or_else(|| crate::error::AppError::Oss {
54            code: None,
55            msg: format!("Bucket 名称格式错误: {bucket}"),
56        })
57}
58
59/// 构建 `PutObject` 权限策略
60///
61/// 限定仅允许对指定对象执行 PUT 上传,且文件大小不超过 `max_bytes`。
62///
63/// # 参数
64///
65/// - `bucket`: COS 存储桶名(含 appId)
66/// - `region`: COS 区域,如 `"ap-guangzhou"`
67/// - `object_key`: 对象 Key(上传路径)
68/// - `max_bytes`: 允许的最大文件字节数
69///
70/// # 返回值
71///
72/// 返回 COS 策略对象,可直接传入 [`get_federation_token`]。
73pub fn build_put_policy(
74    bucket: &str,
75    region: &str,
76    object_key: &str,
77    max_bytes: u64,
78) -> crate::error::ApiResult<Policy> {
79    let app_id = extract_app_id(bucket)?;
80    let resource = format!("qcs::cos:{region}:uid/{app_id}:{bucket}/{object_key}");
81    let mut condition = HashMap::new();
82    condition.insert(
83        "cos:content-length".to_string(),
84        serde_json::json!(max_bytes),
85    );
86    Ok(Policy {
87        version: "2.0".into(),
88        statement: vec![Statement {
89            effect: "allow".into(),
90            action: vec!["name/cos:PutObject".into()],
91            resource: vec![resource],
92            condition: Some(HashMap::from([(
93                "numeric_less_than_equal".into(),
94                condition,
95            )])),
96        }],
97    })
98}
99
100/// 构建 `GetObject` 权限策略
101///
102/// 限定仅允许对指定对象执行 GET 下载。
103///
104/// # 参数
105///
106/// - `bucket`: COS 存储桶名
107/// - `region`: COS 区域
108/// - `object_key`: 对象 Key
109pub fn build_get_policy(
110    bucket: &str,
111    region: &str,
112    object_key: &str,
113) -> crate::error::ApiResult<Policy> {
114    let app_id = extract_app_id(bucket)?;
115    let resource = format!("qcs::cos:{region}:uid/{app_id}:{bucket}/{object_key}");
116    Ok(Policy {
117        version: "2.0".into(),
118        statement: vec![Statement {
119            effect: "allow".into(),
120            action: vec!["name/cos:GetObject".into()],
121            resource: vec![resource],
122            condition: None,
123        }],
124    })
125}
126
127/// 调用 STS `GetFederationToken` 获取临时密钥
128///
129/// 使用腾讯云主密钥换取联合身份临时凭证,
130/// 前端凭此凭证在有效期内直接访问 COS。
131///
132/// # 参数
133///
134/// - `secret_id`: 腾讯云主账号 SecretId
135/// - `secret_key`: 腾讯云主账号 SecretKey
136/// - `region`: COS 区域
137/// - `policy`: 权限策略(由 [`build_put_policy`] 或 [`build_get_policy`] 生成)
138/// - `duration_secs`: 临时凭证有效期(秒)
139///
140/// # 返回值
141///
142/// 返回包含临时密钥和 SessionToken 的 [`TempCredentials`]。
143///
144/// # Errors
145///
146/// - `AppError::Oss` — SecretId/SecretKey 为空或 STS 接口调用失败
147///
148/// # Panics
149///
150/// 不 panic。所有错误通过 `ApiResult` 返回。
151pub async fn get_federation_token(
152    secret_id: &str,
153    secret_key: &str,
154    region: &str,
155    policy: Policy,
156    duration_secs: u64,
157) -> crate::error::ApiResult<TempCredentials> {
158    if secret_id.is_empty() || secret_key.is_empty() {
159        return Err(crate::error::AppError::Oss {
160            code: None,
161            msg: "腾讯云 SecretId/SecretKey 未配置".into(),
162        });
163    }
164    let client = StsClient::new(
165        secret_id.trim().to_string(),
166        secret_key.trim().to_string(),
167        region.to_string(),
168    );
169    let creds = client
170        .get_credentials(GetCredentialsRequest {
171            policy,
172            duration_seconds: Some(duration_secs as u32),
173            name: Some("cos-sts-rust".into()),
174        })
175        .await
176        .map_err(|e| crate::error::AppError::Oss {
177            code: None,
178            msg: format!("STS 失败: {e}"),
179        })?;
180    Ok(TempCredentials {
181        tmp_secret_id: creds.tmp_secret_id,
182        tmp_secret_key: creds.tmp_secret_key,
183        session_token: creds.token,
184    })
185}