基于原始 STS 临时密钥脚本,封装为 FastAPI HTTP 接口,可通过请求获取临时上传凭证上传图片至存储桶。
目录
2)创建/准备子账号与密钥(SecretId/SecretKey)
[3)配置 STS 相关权限(本页为 GetFederationToken / 临时联合身份方式)](#3)配置 STS 相关权限(本页为 GetFederationToken / 临时联合身份方式))
[4)CORS 与(可选)回源域名](#4)CORS 与(可选)回源域名)
[五、UniApp 客户端上传示例](#五、UniApp 客户端上传示例)
一、前置流程(创建存储桶与权限配置)
COS 存储桶、访问权限、以及 STS 所需策略配置
1)创建存储桶(Bucket)
https://console.cloud.tencent.com/cos/bucket
- 登录腾讯云控制台 → 对象存储 COS。
- 新建存储桶:选择所属地域(例如
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 或导致临时密钥拿到后仍无权上传。
子用户最少需要绑定哪些策略?

如果你不追求最小化,只想"能跑通",通常 两条就够:
- STS 权限 :
QcloudSTSFullAccess(允许签发临时密钥)- COS 数据权限 :建议先用
QcloudCOSDataFullControl(对象数据读/写/删),最省事、最不容易踩坑
4)CORS 与(可选)回源域名
- 如果前端(Web/H5)直传 COS,需要在存储桶上配置 CORS 规则允许你的业务域名。

- 小程序端需在小程序后台将 COS 域名配置为合法请求域名。
5)准备运行环境变量
后端建议通过环境变量注入敏感信息:
SecretId:子账号密钥 IDSecretKey:子账号密钥 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 :
GET或POST - 参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| 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>

