腾讯云 COS STS 临时密钥上传

基于原始 STS 临时密钥脚本,封装为 FastAPI HTTP 接口,可通过请求获取临时上传凭证上传图片至存储桶。

目录

一、前置流程(创建存储桶与权限配置)

1)创建存储桶(Bucket)

2)创建/准备子账号与密钥(SecretId/SecretKey)

[3)配置 STS 相关权限(本页为 GetFederationToken / 临时联合身份方式)](#3)配置 STS 相关权限(本页为 GetFederationToken / 临时联合身份方式))

子用户最少需要绑定哪些策略?

[4)CORS 与(可选)回源域名](#4)CORS 与(可选)回源域名)

5)准备运行环境变量

二、依赖安装

三、完整代码

四、接口说明

请求

成功响应

错误响应

[五、UniApp 客户端上传示例](#五、UniApp 客户端上传示例)

完整上传工具函数

页面中调用示例

使用演示


一、前置流程(创建存储桶与权限配置)

COS 存储桶、访问权限、以及 STS 所需策略配置

1)创建存储桶(Bucket)

https://console.cloud.tencent.com/cos/bucket

  1. 登录腾讯云控制台 → 对象存储 COS。
  2. 新建存储桶:选择所属地域(例如 ap-guangzhou),并记录存储桶名称与 appId

存储桶命名通常是 bucketname-appId 形式(示例:bucket-125000000)。

根据业务选择访问权限:

  • 如果只允许通过临时密钥上传,通常存储桶本身保持"私有读写"。
  • 若需要公开访问已上传对象,可结合 CDN 或单独设置对象 ACL(不建议默认公开)

2)创建/准备子账号与密钥(SecretId/SecretKey)

https://console.cloud.tencent.com/cam/overview

在 访问管理 CAM 创建子用户(不要直接使用主账号密钥)。

为子用户创建 API 访问密钥(获得 SecretId / SecretKey),后端服务通过环境变量读取。

3)配置 STS 相关权限(本页为 GetFederationToken / 临时联合身份方式)

使用 qcloud-python-sts 直接传入子用户的 SecretId/SecretKey + policy 来获取临时凭证,属于 STS 的 GetFederationToken(临时联合身份) 这条路(不需要创建角色 Role)。

要点:

  • 子用户必须具备 调用 STS 的权限,否则后端无法签发临时密钥。
  • 子用户还必须具备 COS 权限上限 (至少覆盖你在 policy.statement.action/resource 里声明的权限),否则 STS 可能拒绝该 policy 或导致临时密钥拿到后仍无权上传。
子用户最少需要绑定哪些策略?

如果你不追求最小化,只想"能跑通",通常 两条就够

  1. STS 权限QcloudSTSFullAccess(允许签发临时密钥)
  2. COS 数据权限 :建议先用 QcloudCOSDataFullControl(对象数据读/写/删),最省事、最不容易踩坑

4)CORS 与(可选)回源域名

  • 如果前端(Web/H5)直传 COS,需要在存储桶上配置 CORS 规则允许你的业务域名。
  • 小程序端需在小程序后台将 COS 域名配置为合法请求域名。

5)准备运行环境变量

后端建议通过环境变量注入敏感信息:

  • SecretId:子账号密钥 ID
  • SecretKey:子账号密钥 Key

二、依赖安装

复制代码
pip install fastapi uvicorn qcloud-python-sts

三、完整代码

以下参数需要根据自己的进行修改

appId、secretId、secretKey、bucket 、region

python 复制代码
#!/usr/bin/env python
# coding=utf-8

import os
import datetime
import random

from fastapi import FastAPI, Query, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import Optional
from sts.sts import Sts

app = FastAPI(title="COS STS 临时密钥服务")

# ======================== 配置参数 ========================
CONFIG = {
    "appId": "125000000",  
    "secretId": os.getenv("SecretId"),
    "secretKey": os.getenv("SecretKey"),
    "durationSeconds": 1800,
    "bucket": "bucket-125000000",  #需要根据自己的进行修改
    "region": "ap-guangzhou",      #需要根据自己的进行修改
    "allowActions": [
        "name/cos:PutObject",
        "name/cos:InitiateMultipartUpload",
        "name/cos:ListMultipartUploads",
        "name/cos:ListParts",
        "name/cos:UploadPart",
        "name/cos:CompleteMultipartUpload",
    ],
}

PERMISSION = {
    "limitExt": False,
    "extWhiteList": ["jpg", "jpeg", "png", "gif", "bmp"],
    "limitContentType": False,
    "limitContentLength": False,
}

# ======================== 请求模型 ========================
class CredentialRequest(BaseModel):
    filename: str

# ======================== 工具函数 ========================
def generate_cos_key(ext: Optional[str] = None) -> str:
    """生成 COS 文件路径,格式: file/YYYYMMDD/YYYYMMDD_XXXXXX.ext"""
    date = datetime.datetime.now()
    ymd = date.strftime("%Y%m%d")
    r = str(int(random.random() * 1000000)).zfill(6)
    return f"file/{ymd}/{ymd}_{r}.{ext if ext else ''}"

def build_condition() -> dict:
    """根据权限配置构建 STS policy condition"""
    condition = {}
    if PERMISSION["limitContentType"]:
        condition["string_like_if_exist"] = {
            "cos:content-type": "image/*"
        }
    if PERMISSION["limitContentLength"]:
        condition["numeric_less_than_equal"] = {
            "cos:content-length": 5 * 1024 * 1024
        }
    return condition

def get_sts_credential(filename: str) -> dict:
    """
    核心逻辑:根据文件名生成 COS Key,申请 STS 临时密钥并返回。
    """
    segments = filename.split(".")
    ext = segments[-1].lower() if len(segments) > 1 else ""

    # 后缀白名单校验
    if PERMISSION["limitExt"]:
        if not ext or ext not in PERMISSION["extWhiteList"]:
            raise HTTPException(
                status_code=403,
                detail=f"不允许的文件后缀: .{ext}"
            )

    key = generate_cos_key(ext)
    resource = (
        f"qcs::cos:{CONFIG['region']}:uid/{CONFIG['appId']}:"
        f"{CONFIG['bucket']}/{key}"
    )
    condition = build_condition()

    credential_option = {
        "duration_seconds": CONFIG["durationSeconds"],
        "secret_id": CONFIG["secretId"],
        "secret_key": CONFIG["secretKey"],
        "bucket": CONFIG["bucket"],
        "region": CONFIG["region"],
        "policy": {
            "version": "2.0",
            "statement": [
                {
                    "action": CONFIG["allowActions"],
                    "effect": "allow",
                    "resource": [resource],
                    "condition": condition,
                }
            ],
        },
    }

    sts = Sts(credential_option)
    response = sts.get_credential()
    credential_dic = dict(response)
    credential_info = credential_dic.get("credentials", {})

    return {
        "bucket": CONFIG["bucket"],
        "region": CONFIG["region"],
        "key": key,
        "startTime": credential_dic.get("startTime"),
        "expiredTime": credential_dic.get("expiredTime"),
        "requestId": credential_dic.get("requestId"),
        "expiration": credential_dic.get("expiration"),
        "credentials": {
            "tmpSecretId": credential_info.get("tmpSecretId"),
            "tmpSecretKey": credential_info.get("tmpSecretKey"),
            "sessionToken": credential_info.get("sessionToken"),
        },
    }

# ======================== 接口路由 ========================
@app.get("/api/cos/credential")
async def get_credential_via_get(
    filename: str = Query(..., description="要上传的文件名,如 photo.jpg")
):
    """GET 方式获取 COS 临时上传凭证"""
    try:
        data = get_sts_credential(filename)
        return {"code": 0, "message": "ok", "data": data}
    except HTTPException:
        raise
    except Exception as e:
        return JSONResponse(
            status_code=500,
            content={"code": -1, "message": f"服务器内部错误: {e}"}
        )

@app.post("/api/cos/credential")
async def get_credential_via_post(req: CredentialRequest):
    """POST 方式获取 COS 临时上传凭证"""
    try:
        data = get_sts_credential(req.filename)
        return {"code": 0, "message": "ok", "data": data}
    except HTTPException:
        raise
    except Exception as e:
        return JSONResponse(
            status_code=500,
            content={"code": -1, "message": f"服务器内部错误: {e}"}
        )

# ======================== 启动 ========================
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=5000)

四、接口说明

请求

  • URL : /api/cos/credential
  • Method : GETPOST
  • 参数:
参数名 类型 必填 说明
filename string 要上传的文件名,如 photo.jpg

GET 示例

复制代码
GET /api/cos/credential?filename=photo.jpg

POST 示例

复制代码
POST /api/cos/credential
Content-Type: application/json

{"filename": "photo.jpg"}

成功响应

复制代码
{
    "code": 0,
    "message": "ok",
    "data": {
        "bucket": "bucket-125000000",
        "region": "ap-guangzhou",
        "key": "file/20260317/20260317_382910.jpg",
        "startTime": 1742169600,
        "expiredTime": 1742171400,
        "requestId": "xxx",
        "expiration": "2026-03-17T03:30:00Z",
        "credentials": {
            "tmpSecretId": "AKIDxxxxxxxx",
            "tmpSecretKey": "xxxxxxxx",
            "sessionToken": "xxxxxxxx"
        }
    }
}

错误响应

复制代码
{"code": -1, "message": "缺少参数 filename"}

{"code": -1, "message": "不允许的文件后缀: .exe"}

启动方式

python 复制代码
python main.py

启动后访问 http://localhost:5000/docs 可查看自动生成的 Swagger 交互文档。

五、UniApp 客户端上传示例

在 UniApp 项目中安装 COS SDK:

复制代码
npm install cos-js-sdk-v5

完整上传工具函数

javascript 复制代码
import COS from 'cos-js-sdk-v5';

// 临时密钥服务地址
const STS_BASE_URL = 'http://127.0.0.1:5000';

/**
 * 从后端获取 COS 临时密钥
 * @param {string} filename - 文件名,如 "photo.jpg"
 * @returns {Promise<object>} 临时密钥信息
 */
function getCredential(filename) {
  return new Promise((resolve, reject) => {
    uni.request({
      url: `${STS_BASE_URL}/api/cos/credential`,
      method: 'GET',
      data: { filename },
      success: (res) => {
        if (res.data && res.data.code === 0) {
          resolve(res.data.data);
        } else {
          reject(new Error(res.data?.message || '获取临时密钥失败'));
        }
      },
      fail: (err) => {
        reject(new Error('请求临时密钥接口失败: ' + JSON.stringify(err)));
      },
    });
  });
}

/**
 * 选择图片并上传到 COS
 * @returns {Promise<object>} 上传结果
 */
export function chooseAndUpload() {
  return new Promise((resolve, reject) => {
    // 1. 选择图片
    uni.chooseImage({
      count: 1,
      sizeType: ['compressed'],
      sourceType: ['album', 'camera'],
      success: async (chooseRes) => {
		const file = chooseRes.tempFiles[0];
		const fileName = file.name;
		
        try {
          // 2. 获取临时密钥
          const credential = await getCredential(fileName);
          const {
            credentials = {},
            startTime,
            expiredTime,
            bucket,
            region,
            key,
          } = credential;
          const { tmpSecretId, tmpSecretKey, sessionToken } = credentials;

          // 3. 校验参数
          const params = { tmpSecretId, tmpSecretKey, sessionToken, bucket, region, key };
          const emptyParam = Object.keys(params).find((k) => !params[k]);
          if (emptyParam) {
            reject(new Error(`参数错误: ${emptyParam} 不能为空`));
            return;
          }

          // 4. 创建 COS 实例
          const cos = new COS({
            SecretId: tmpSecretId,
            SecretKey: tmpSecretKey,
            SecurityToken: sessionToken,
            StartTime: startTime,
            ExpiredTime: expiredTime,
          });

          // 5. 上传文件
          cos.uploadFile(
            {
              Bucket: bucket,
              Region: region,
              Key: key,
              Body: file,    //文件对象
              onProgress: (progressData) => {
                const percent = Math.floor(progressData.percent * 100);
                console.log(`上传进度: ${percent}%`);
                // 可通过事件总线或回调更新 UI 进度条
              },
            },
            (err, data) => {
              if (err) {
                console.error('上传失败:', err);
                reject(err);
              } else {
                console.log('上传成功:', data);
                // data.Location 即为文件的访问 URL
                resolve(data);
              }
            }
          );
        } catch (e) {
          reject(e);
        }
      },
      fail: (err) => {
        reject(new Error('选择图片失败: ' + JSON.stringify(err)));
      },
    });
  });
}


/**
 * 选择视频并上传到 COS
 * @param {number} maxDuration - 最大录制时长(秒),默认 60
 * @returns {Promise<object>} 上传结果
 */
export function chooseAndUploadVideo(maxDuration = 60) {
  return new Promise((resolve, reject) => {
    // 1. 选择视频
    uni.chooseVideo({
      sourceType: ['album', 'camera'],
      maxDuration,
      compressed: true,
      success: async (chooseRes) => {
        const File = chooseRes.tempFile;
        const fileName = File.name;
		
		console.log(111111,fileName)
        try {
          // 2. 获取临时密钥
          const credential = await getCredential(fileName);
          const {
            credentials = {},
            startTime,
            expiredTime,
            bucket,
            region,
            key,
          } = credential;
          const { tmpSecretId, tmpSecretKey, sessionToken } = credentials;
          // 3. 校验参数
          const params = { tmpSecretId, tmpSecretKey, sessionToken, bucket, region, key };
          const emptyParam = Object.keys(params).find((k) => !params[k]);
          if (emptyParam) {
            reject(new Error(`参数错误: ${emptyParam} 不能为空`));
            return;
          }

          // 4. 创建 COS 实例
          const cos = new COS({
            SecretId: tmpSecretId,
            SecretKey: tmpSecretKey,
            SecurityToken: sessionToken,
            StartTime: startTime,
            ExpiredTime: expiredTime,
          });

          // 5. 上传视频
          cos.uploadFile(
            {
              Bucket: bucket,
              Region: region,
              Key: key,
              Body: File,
              onProgress: (progressData) => {
                const percent = Math.floor(progressData.percent * 100);
                console.log(`视频上传进度: ${percent}%`);
              },
            },
            (err, data) => {
              if (err) {
                console.error('视频上传失败:', err);
                reject(err);
              } else {
                console.log('视频上传成功:', data);
                resolve(data);
              }
            }
          );
        } catch (e) {
          reject(e);
        }
      },
      fail: (err) => {
        reject(new Error('选择视频失败: ' + JSON.stringify(err)));
      },
    });
  });
}

/**
 * 直接上传指定文件路径到 COS(适用于已知路径的场景)
 * @param {string} file - 文件对象
 * @param {string} fileName - 文件名,如 "video.mp4"
 * @returns {Promise<object>} 上传结果
 */
export function uploadFile(file, fileName) {
  return new Promise(async (resolve, reject) => {
    try {
      const credential = await getCredential(fileName);
      const {
        credentials = {},
        startTime,
        expiredTime,
        bucket,
        region,
        key,
      } = credential;
      const { tmpSecretId, tmpSecretKey, sessionToken } = credentials;

      const cos = new COS({
        SecretId: tmpSecretId,
        SecretKey: tmpSecretKey,
        SecurityToken: sessionToken,
        StartTime: startTime,
        ExpiredTime: expiredTime,
      });

      cos.uploadFile(
        {
          Bucket: bucket,
          Region: region,
          Key: key,
          Body: file,
          onProgress: (progressData) => {
            console.log(`上传进度: ${Math.floor(progressData.percent * 100)}%`);
          },
        },
        (err, data) => {
          if (err) {
            reject(err);
          } else {
            resolve(data);
          }
        }
      );
    } catch (e) {
		
		
      reject(e);
    }
  });
}

页面中调用示例

javascript 复制代码
<template>
  <view class="container">
    <button @click="handleUploadImage">选择图片并上传</button>
    <button @click="handleUploadVideo">选择视频并上传</button>
    <text v-if="uploading">上传中...</text>
    <image v-if="imageUrl" :src="imageUrl" mode="widthFix" />
    <video v-if="videoUrl" :src="videoUrl" controls />
  </view>
</template>

<script>
import { chooseAndUpload, chooseAndUploadVideo } from '@/utils/cos-upload.js';

export default {
  data() {
    return {
      uploading: false,
      imageUrl: '',
      videoUrl: '',
    };
  },
  methods: {
    async handleUploadImage() {
      this.uploading = true;
      try {
        const result = await chooseAndUpload();
        this.imageUrl = `https://${result.Location}`;
        uni.showToast({ title: '图片上传成功', icon: 'success' });
      } catch (e) {
        console.error(e);
        uni.showToast({ title: '图片上传失败', icon: 'none' });
      } finally {
        this.uploading = false;
      }
    },
    async handleUploadVideo() {
      this.uploading = true;
      try {
        const result = await chooseAndUploadVideo(60);
        this.videoUrl = `https://${result.Location}`;
        uni.showToast({ title: '视频上传成功', icon: 'success' });
      } catch (e) {
        console.error(e);
        uni.showToast({ title: '视频上传失败', icon: 'none' });
      } finally {
        this.uploading = false;
      }
    },
  },
};
</script>

使用演示

相关推荐
2401_874732531 小时前
构建一个桌面版的天气预报应用
jvm·数据库·python
qq_417695051 小时前
实战:用Python开发一个简单的区块链
jvm·数据库·python
我的xiaodoujiao2 小时前
3、API 接口自动化测试详细图文教程学习系列3--相关Python基础知识2
python·学习·测试工具·pytest
南 阳2 小时前
Python从入门到精通day56
开发语言·python
阿kun要赚马内2 小时前
Python中函数的进阶用法
开发语言·python
Spliceㅤ2 小时前
项目:基于qwen的点餐系统
开发语言·人工智能·python·机器学习·自然语言处理
asdzx672 小时前
使用 Python 快速为 PDF 添加背景色或背景图片
python·pdf
badhope2 小时前
Docker入门到实战全攻略
linux·python·docker·github·matplotlib
华研前沿标杆游学2 小时前
2026深圳企业参访-走进深圳华星光电TCL学习智能制造
python
dapeng28702 小时前
Python异步编程入门:Asyncio库的使用
jvm·数据库·python