服务端签名实践

最近更新时间:2025-05-26 20:07:42

我的收藏
本文档介绍如何搭建安全的临时签名服务。

方案优势

权限安全:可以有效限定安全的权限范围,只能用于上传指定的一个文件路径。
路径安全:由服务端决定随机的 COS 文件路径,可以有效避免已有文件被覆盖的问题和安全风险。
传输安全:在服务端生成签名,避免临时密钥传输过程中泄漏的风险。

上传流程

1. 客户端选择文件,客户端将原始文件名发送给服务端。
2. 服务端根据文件名后缀,生成带时间的随机 COS 文件路径,并申请对应权限的签名信息和 cos key 返回给前端。

临时签名服务搭建

临时密钥(临时访问凭证)是通过 CAM 云 API 提供的接口,获取到权限受限的密钥。当需要发起 COS API 请求时,需要用到获取临时密钥接口返回信息中 的 TmpSecretId、TmpSecretKey 和 Token 三个字段,用于计算签名。
在请求服务端签名时,会先获取临时密钥,然后用临时密钥生成签名,返回客户端。
以下示例各语言示例代码:
Node.js
Go
PHP
Python
Java
.NET(C#)
完整代码可参考 示例代码
// 临时密钥服务例子
var express = require('express');
var crypto = require('crypto');
var moment = require('moment');
var STS = require('qcloud-cos-sts');
const url = require('url');
const { log } = require('console');

// 配置参数
var config = {
// 获取腾讯云密钥,建议使用限定权限的子用户的密钥 https://console.cloud.tencent.com/cam/capi
secretId: process.env.COS_SECRET_ID,
secretKey: process.env.COS_SECRET_KEY,
// 密钥有效期
durationSeconds: 1800,
// 这里填写存储桶、地域,例如:test-1250000000、ap-guangzhou
bucket: process.env.PERSIST_BUCKET,
region: process.env.PERSIST_BUCKET_REGION
};

// 获取临时密钥
var getTempCredential = async function(cosKey){
var shortBucketName = config.bucket.substr(0 , config.bucket.lastIndexOf('-'));
var appId = config.bucket.substr(1 + config.bucket.lastIndexOf('-'));
// 开始获取临时密钥
var policy = {
"version": "2.0",
"statement": [
{
"action": [
// 上传需要的操作
"name/cos:PutObject"
],
"effect": "allow",
"resource": [
// 仅限cosKey资源
'qcs::cos:' + config.region + ':uid/' + appId + ':prefix//' + appId + '/' + shortBucketName + '/' + cosKey,
]
}
]
};
let tempKeys = null;
try{
tempKeys = await STS.getCredential({
secretId: config.secretId,
secretKey: config.secretKey,
durationSeconds: config.durationSeconds,
policy: policy,
});
console.log(tempKeys);
return tempKeys;
} catch(err){
console.log(err);
res.send(JSON.stringify(err));
return null;
}
};

// 计算签名
var getSignature = function(tempCredential, httpMethod, cosHost, pathname) {
const signAlgorithm = 'sha1';
const credentials = tempCredential.credentials;
const keyTime = `${tempCredential.startTime};${tempCredential.expiredTime}`;

// 步骤一:生成 SignKey
var signKey = crypto.createHmac(signAlgorithm, credentials.tmpSecretKey).update(keyTime).digest('hex');
console.log("signKey:"+signKey);

// 步骤二:生成 StringToSign
const httpString = `${httpMethod.toLowerCase()}\\n/${pathname}\\n\\nhost=${cosHost}\\n`;
console.log("httpString:"+httpString);
const httpStringHash = crypto.createHash(signAlgorithm).update(httpString).digest('hex');
const stringToSign = `${signAlgorithm}\\n${keyTime}\\n${httpStringHash}\\n`;
console.log("stringToSign:"+stringToSign);

// 步骤三:生成 Signature
var signature = crypto.createHmac(signAlgorithm, signKey).update(stringToSign).digest('hex');
console.log("signature:"+signature);

// 步骤四:生成 authorization
let authorization = `q-sign-algorithm=${signAlgorithm}&
q-ak=${credentials.tmpSecretId}&
q-sign-time=${keyTime}&
q-key-time=${keyTime}&
q-header-list=host&q-url-param-list=&q-signature=${signature}`;

// 去掉掉上面换行导致的\\n
authorization = authorization.replace(/\\n/g, '');
console.log("authorization:"+authorization);
return authorization;
}

// 创建临时密钥服务和用于调试的静态服务
var app = express();
// 直传签名接口
app.all('/sts-server-sign', async function (req, res, next) {
// 获取需要签名的字段
var httpMethod = req.query.httpMethod;
var cosHost = req.query.host;
var cosKey = req.query.cosKey;

console.log(httpMethod + " " + cosHost + " " + cosKey);

cosHost = decodeURIComponent(cosHost);
cosKey = decodeURIComponent(cosKey);

console.log(httpMethod + " " + cosHost + " " + cosKey);

// 判断异常情况
if (!config.secretId || !config.secretKey) return res.send({ code: '-1', message: 'secretId or secretKey not ready' });
if (!config.bucket || !config.region) return res.send({ code: '-1', message: 'bucket or regions not ready' });
if (!httpMethod || !cosHost || !cosKey) return res.send({ code: '-1', message: 'httpMethod or host or coskey is not empty' });

// 开始获取临时密钥
var tempCredential = await getTempCredential(cosKey);
if(!tempCredential){
res.send({ code: -1, message: 'get temp credentials fail' });
return;
}

// 用临时密钥计算签名
let authorization = getSignature(tempCredential, httpMethod, cosHost, cosKey);

// 返回域名、文件路径、签名、凭证信息
res.send({
code: 0,
data: {
cosHost: cosHost,
cosKey: cosKey,
authorization: authorization,
securityToken: tempCredential.credentials.sessionToken
},
});
});

app.all('*', function (req, res, next) {
res.send({ code: -1, message: '404 Not Found' });
});

// 启动签名服务
app.listen(3000);
console.log('app is listening at http://127.0.0.1:3000');

完整代码可参考 示例代码
package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/tencentyun/cos-go-sdk-v5"
sts "github.com/tencentyun/qcloud-cos-sts-sdk"
"math/rand"
"net/http"
"net/url"
"os"
"reflect"
"strings"
"time"
"unicode"
)

type Config struct {
filename string
appId string
SecretId string
SecretKey string
Proxy string
DurationSeconds int
Bucket string
Region string
AllowActions []string
}

type Permission struct {
LimitExt bool `json:"limitExt"`
ExtWhiteList []string `json:"extWhiteList"`
LimitContentType bool `json:"limitContentType"`
LimitContentLength bool `json:"limitContentLength"`
}

func generateCosKey(ext string) string {
date := time.Now()
m := int(date.Month()) + 1
ymd := fmt.Sprintf("%d%02d%d", date.Year(), m, date.Day())
r := fmt.Sprintf("%06d", rand.Intn(1000000))
cosKey := fmt.Sprintf("file/%s/%s_%s.%s", ymd, ymd, r, ext)
return cosKey
}

func getPermission() Permission {
permission := Permission{
LimitExt: true,
ExtWhiteList: []string{"jpg", "jpeg", "png", "gif", "bmp"},
LimitContentType: false,
LimitContentLength: false,
}
return permission
}

func getConfig() Config {
config := Config{
filename: "test.jpg",
appId: "12500000000",
SecretId: os.Getenv("SECRETID"), // 用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参考https://cloud.tencent.com/document/product/598/37140
SecretKey: os.Getenv("SECRETKEY"), // 用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参考https://cloud.tencent.com/document/product/598/37140
Proxy: os.Getenv("Proxy"),
DurationSeconds: 1800,
Bucket: "0-1253960454",
Region: "ap-guangzhou",
AllowActions: []string{
"name/cos:PutObject"
},
}
return config
}

func stringInSlice(str string, list []string) bool {
for _, v := range list {
if v == str {
return true
}
}
return false
}

func StructToCamelMap(input interface{}) map[string]interface{} {
v := reflect.ValueOf(input)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}

result := make(map[string]interface{})
typ := v.Type()

for i := 0; i < v.NumField(); i++ {
field := typ.Field(i)
fieldValue := v.Field(i)

// 转换字段名为小驼峰
key := toLowerCamel(field.Name)

// 处理嵌套结构体
if fieldValue.Kind() == reflect.Struct ||
(fieldValue.Kind() == reflect.Ptr && fieldValue.Elem().Kind() == reflect.Struct) {

if fieldValue.IsNil() && fieldValue.Kind() == reflect.Ptr {
result[key] = nil
continue
}

result[key] = StructToCamelMap(fieldValue.Interface())
} else {
// 处理基本类型
result[key] = fieldValue.Interface()
}
}

return result
}

// 转换为小驼峰格式(首字母小写)
func toLowerCamel(s string) string {
if s == "" {
return s
}

// 处理全大写单词(如 ID)
if strings.ToUpper(s) == s {
return strings.ToLower(s)
}

// 普通小驼峰转换
runes := []rune(s)
runes[0] = unicode.ToLower(runes[0])
return string(runes)
}

func getStsCredential() (map[string]interface{}, error) {
config := getConfig()

permission := getPermission()

c := sts.NewClient(
// 通过环境变量获取密钥, os.Getenv 方法表示获取环境变量
config.SecretId, //os.Getenv("SECRETID"), // 用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参考https://cloud.tencent.com/document/product/598/37140
config.SecretKey, //os.Getenv("SECRETKEY"), // 用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参考https://cloud.tencent.com/document/product/598/37140
nil,
// sts.Host("sts.internal.tencentcloudapi.com"), // 设置域名, 默认域名sts.tencentcloudapi.com
// sts.Scheme("http"), // 设置协议, 默认为https,公有云sts获取临时密钥不允许走http,特殊场景才需要设置http
)

condition := make(map[string]map[string]interface{})

segments := strings.Split(config.filename, ".")
if len(segments) == 0 {
//ext := ""
}
ext := segments[len(segments)-1]

if permission.LimitExt {
extInvalid := ext == "" || !stringInSlice(ext, permission.ExtWhiteList)
if extInvalid {
return nil, errors.New("非法文件,禁止上传")
}
}

if permission.LimitContentType {
condition["string_like_if_exist"] = map[string]interface{}{
// 只允许上传 content-type 为图片类型
"cos:content-type": "image/*",
}
}

// 3. 限制上传文件大小
if permission.LimitContentLength {
condition["numeric_less_than_equal"] = map[string]interface{}{
// 上传大小限制不能超过 5MB(只对简单上传生效)
"cos:content-length": 5 * 1024 * 1024,
}
}

key := generateCosKey(ext)
// 策略概述 https://cloud.tencent.com/document/product/436/18023
opt := &sts.CredentialOptions{
DurationSeconds: int64(config.DurationSeconds),
Region: config.Region,
Policy: &sts.CredentialPolicy{
Version: "2.0",
Statement: []sts.CredentialPolicyStatement{
{
// 密钥的权限列表。简单上传和分片需要以下的权限,其他权限列表请看 https://cloud.tencent.com/document/product/436/31923
Action: config.AllowActions,
Effect: "allow",
Resource: []string{
// 这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的具体路径,例子: a.jpg 或者 a/* 或者 * (使用通配符*存在重大安全风险, 请谨慎评估使用)
// 存储桶的命名格式为 BucketName-APPID,此处填写的 bucket 必须为此格式
"qcs::cos:ap-guangzhou:uid/" + config.appId + ":" + config.Bucket + "/" + key,
},
// 开始构建生效条件 condition
// 关于 condition 的详细设置规则和COS支持的condition类型可以参考https://cloud.tencent.com/document/product/436/71306
Condition: condition,
},
},
},
}

// case 1 请求临时密钥
res, err := c.GetCredential(opt)
if err != nil {
return nil, err
}
// 转换为小驼峰 map
resultMap := StructToCamelMap(res)
resultMap["bucket"] = config.Bucket
resultMap["region"] = config.Region
resultMap["key"] = key

return resultMap, nil
}

func main() {
result, err := getStsCredential()
if err != nil {
fmt.Printf("请求临时密钥失败: %v\\n", err)
return // 根据上下文决定是否退出
}

// 类型断言 credentials 为 map[string]interface{}
credentials, ok := result["credentials"].(map[string]interface{})
if !ok {
fmt.Println("凭证格式错误")
return
}
// 带类型检查的字段获取
tak, tok := credentials["tmpSecretID"].(string)
tsk, tskok := credentials["tmpSecretKey"].(string)
token, tokenok := credentials["sessionToken"].(string)
if !tok || !tskok || !tokenok {
fmt.Println("临时凭证字段缺失或类型错误")
return
}
host := "https://" + result["bucket"].(string) + ".cos." + result["region"].(string) + ".myqcloud.com"
u, _ := url.Parse(host)
b := &cos.BaseURL{BucketURL: u}
c := cos.NewClient(b, &http.Client{})
name := result["key"].(string)
ctx := context.Background()
opt := &cos.PresignedURLOptions{
Query: &url.Values{},
Header: &http.Header{},
}
opt.Query.Add("x-cos-security-token", token)
signature := c.Object.GetSignature(ctx, http.MethodPut, name, "tak", "tsk", time.Hour, opt, true)
fmt.Printf("%s%s%s%s", tak, tsk, signature, token)
data := make(map[string]string)

// 添加token和sign
data["securityToken"] = token
data["authorization"] = signature
data["cosHost"] = host
data["cosKey"] = name
// 转换为JSON
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
fmt.Println("JSON编码失败:", err)
return
}
fmt.Println(string(jsonData))
}


完整代码可参考 示例代码
<?php
require_once __DIR__ . '/vendor/autoload.php';

use QCloud\\COSSTS\\Sts;

// 生成要上传的 COS 文件路径文件名
function generateCosKey($ext) {
$ymd = date('Ymd');
$r = substr('000000' . rand(), -6);
$cosKey = 'file/' . $ymd. '/' . $ymd . '_' . $r;
if ($ext) {
$cosKey = $cosKey . '.' . $ext;
}
return $cosKey;
};

// 获取单一文件上传权限的临时密钥
function getKeyAndCredentials($filename) {
// 业务自行实现 用户登录态校验,比如对 token 校验
// $canUpload = checkUserRole($userToken);
// if (!$canUpload) {
// return '当前用户没有上传权限';
// }

// 上传文件可控制类型、大小,按需开启
$permission = array(
'limitExt' => false, // 限制上传文件后缀
'extWhiteList' => ['jpg', 'jpeg', 'png', 'gif', 'bmp'], // 限制的上传后缀
'limitContentType' => false, // 限制上传 contentType
'limitContentLength' => false, // 限制上传文件大小
);
$condition = array();

// 客户端传进原始文件名,这里根据文件后缀生成随机 Key
$ext = pathinfo($filename, PATHINFO_EXTENSION);

// 1. 限制上传文件后缀
if ($permission['limitExt']) {
if ($ext === '' || array_key_exists($ext, $permission['extWhiteList'])) {
return '非法文件,禁止上传';
}
}

// 2. 限制上传文件 content-type
if ($permission['limitContentType']) {
// 只允许上传 content-type 为图片类型
$condition['string_like_if_exist'] = array('cos:content-type' => 'image/*');
}

// 3. 限制上传文件大小
if ($permission['limitContentLength']) {
// 上传大小限制不能超过 5MB(只对简单上传生效)
$condition['numeric_less_than_equal'] = array('cos:content-length' => 5 * 1024 * 1024);
}

$cosKey = generateCosKey($ext);
$bucket = 'test-125000000'; // 换成你的 bucket
$region = 'ap-guangzhou'; // 换成 bucket 所在园区
$config = array(
'url' => 'https://sts.tencentcloudapi.com/', // url和domain保持一致
'domain' => 'sts.tencentcloudapi.com', // 域名,非必须,默认为 sts.tencentcloudapi.com
'proxy' => '',
'secretId' => "",//getenv('GROUP_SECRET_ID'), // 固定密钥,若为明文密钥,请直接以'xxx'形式填入,不要填写到getenv()函数中
'secretKey' => "",//getenv('GROUP_SECRET_KEY'), // 固定密钥,若为明文密钥,请直接以'xxx'形式填入,不要填写到getenv()函数中
'bucket' => $bucket, // 换成你的 bucket
'region' => $region, // 换成 bucket 所在园区
'durationSeconds' => 1800, // 密钥有效期
'allowPrefix' => array($cosKey), // 只分配当前 key 的路径权限
// 密钥的权限列表。简单上传和分片需要以下的权限,其他权限列表请看 https://cloud.tencent.com/document/product/436/31923
'allowActions' => array (
// 简单上传
'name/cos:PutObject'
),
);

if (!empty($condition)) {
$config['condition'] = $condition;
}

$sts = new Sts();
$tempKeys = $sts->getTempKeys($config);
$resTemp = array_merge(
$tempKeys,
[
'startTime' => time(),
'bucket' => $bucket,
'region' => $region,
'key' => $cosKey,
]
);
return $resTemp;
}

function getSign()
{
$filename = "test.jpg";
$method = "putObject";
$result = getKeyAndCredentials($filename);
$credentials = $result["credentials"];
$sessionToken = $credentials["sessionToken"];
$tmpSecretId = $credentials["tmpSecretId"];
$tmpSecretKey = $credentials["tmpSecretKey"];
$expiredTime = $result["expiredTime"];
$startTime = $result["startTime"];
$bucket = $result["bucket"];
$region = $result["region"];
$key = $result["key"];

$cosClient = new Qcloud\\Cos\\Client(
array(
'region' => $region,
'scheme' => 'https', //协议头部,默认为 http
'signHost' => true, //默认签入Header Host;您也可以选择不签入Header Host,但可能导致请求失败或安全漏洞,若不签入host则填false
'credentials'=> array(
'secretId' => $tmpSecretId,
'secretKey' => $tmpSecretKey,
'token' => $sessionToken)));
### 简单上传预签名
try {
$url = $cosClient->getPresignedUrl($method,array(
'Bucket' => $bucket,
'Key' => $key,
'Body' => "",
'Params'=> array('x-cos-security-token' => $sessionToken),
'Headers'=> array(),
), $expiredTime - $startTime); //签名的有效时间

$parsedUrl = parse_url($url);
$host = 'https://' . $parsedUrl['host']; // 自动包含端口(如果有)
$queryString = isset($parsedUrl['query']) ? $parsedUrl['query'] : '';
$queryParts = explode('&', $queryString);
$signParts = array_filter($queryParts, function($part) {
return strpos($part, 'x-cos-security-token=') !== 0;
});
$sign = implode('&', $signParts);
$result = [
'cosHost' => $host,
'cosKey' => $key,
'authorization' => $sign,
'securityToken' => $sessionToken
];

echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

} catch (\\Exception $e) {
// 请求失败
echo($e);
}
}
getSign();
完整代码可参考 示例代码
#!/usr/bin/env python
# coding=utf-8
import json
import os
import datetime
import random
from urllib.parse import urlencode

from qcloud_cos import CosConfig, CosS3Client

from sts.sts import Sts


if __name__ == '__main__':

# 配置参数
config = {
"filename":"test.jpg",
"appId": "1250000000",
"secretId": os.getenv("SecretId"),
"secretKey": os.getenv("SecretKey"),
"proxy": os.getenv("Proxy"),
"durationSeconds": 1800,
"bucket": "0-1253960454",
"region": "ap-guangzhou",
# 密钥的上传操作权限列表
"allowActions": [
# 简单上传
"name/cos:PutObject"
],
}

permission = {
"limitExt": True, # 限制上传文件后缀
"extWhiteList": ["jpg", "jpeg", "png", "gif", "bmp"], # 限制的上传后缀
"limitContentType": False, # 限制上传 contentType
"limitContentLength": False, # 限制上传文件大小
}


# 生成要上传的 COS 文件路径文件名
def generate_cos_key(ext=None):
date = datetime.datetime.now()
ymd = date.strftime('%Y%m%d')
r = str(int(random.random() * 1000000)).zfill(6)
cos_key = f"file/{ymd}/{ymd}_{r}.{ext if ext else ''}"
return cos_key


segments = config['filename'].split(".")
ext = segments[-1] if segments else ""
key = generate_cos_key(ext)
resource = f"qcs::cos:{config['region']}:uid/{config['appId']}:{config['bucket']}/{key}"

condition = {}

# 1. 限制上传文件后缀
if permission["limitExt"]:
ext_invalid = not ext or ext not in permission["extWhiteList"]
if ext_invalid:
print('非法文件,禁止上传')

# 2. 限制上传文件 content-type
if permission["limitContentType"]:
condition.update({
"string_like_if_exist": {
# 只允许上传 content-type 为图片类型
"cos:content-type": "image/*"
}
})

# 3. 限制上传文件大小
if permission["limitContentLength"]:
condition.update({
"numeric_less_than_equal": {
# 上传大小限制不能超过 5MB(只对简单上传生效)
"cos:content-length": 5 * 1024 * 1024
}
})


def get_credential_demo():
credentialOption = {
# 临时密钥有效时长,单位是秒
'duration_seconds': config.get('durationSeconds'),
'secret_id': config.get("secretId"),
# 固定密钥
'secret_key': config.get("secretKey"),
# 换成你的 bucket
'bucket': config.get("bucket"),
'proxy': config.get("proxy"),
# 换成 bucket 所在地区
'region': config.get("region"),
"policy": {
"version": '2.0',
"statement": [
{
"action": config.get("allowActions"),
"effect": "allow",
"resource": [
resource
],
"condition": condition
}
],
},
}

try:

sts = Sts(credentialOption)
response = sts.get_credential()
credential_dic = dict(response)
credential_info = credential_dic.get("credentials")
credential = {
"bucket": config.get("bucket"),
"region": config.get("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"),
},
}
return credential
except Exception as e:
print(e)


result = get_credential_demo()
credentials = result["credentials"]
secret_id = credentials["tmpSecretId"]
secret_key = credentials["tmpSecretKey"]
token = credentials["sessionToken"]
bucket = result["bucket"]
region = result["region"]
key = result["key"]
expired = result["expiredTime"]

config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token)
client = CosS3Client(config)
sign = client.get_auth(Method='put',Bucket=bucket, Key=key, Expired=expired, Params={
'x-cos-security-token': token
},SignHost=True)
sign = urlencode(dict([item.split('=', 1) for item in sign.split('&')]))
host = "https://" + result["bucket"] + ".cos." + result["region"] + ".myqcloud.com"
response = {
"cosHost":host,
"cosKey":key,
"authorization":sign,
"securityToken":token
}
print('get data : ' + json.dumps(response, indent=4))
完整代码可参考 示例代码
package com.tencent.cloud;

import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicSessionCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.http.HttpMethodName;
import com.qcloud.cos.region.Region;
import com.tencent.cloud.cos.util.Jackson;
import org.junit.Test;

import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.*;

public class ServerSignTest {

public static String generateCosKey(String ext) {
Date date = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
String ymd = dateFormat.format(date);

Random random = new Random();
int r = random.nextInt(1000000);
String rStr = String.format("%06d", r);

String cosKey = String.format("file/%s/%s_%s.%s", ymd, ymd, rStr, ext != null ? ext : "");
return cosKey;
}

// 获取配置信息
public TreeMap<String,Object> getConfig(){

String bucket = "0-1250000000";
String appId = "1250000000";
String filename = "test.jpg";
String region = "ap-guangzhou";
String secretId = "";
String secretKey = "";
String proxy = "";
int durationSeconds = 1800;

String[] segments = filename.split("\\\\.");
String ext = segments.length > 0 ? segments[segments.length - 1] : "";

// 临时密钥限制
Boolean limitExt = false; // 限制上传文件后缀
List extWhiteList = Arrays.asList("jpg", "jpeg", "png", "gif", "bmp"); // 限制的上传后缀
Boolean limitContentType = false; // 限制上传 contentType
Boolean limitContentLength = false; // 限制上传文件大小


Map<String, Object> condition = new HashMap();

// 1. 限制上传文件后缀
if (limitExt) {
boolean extInvalid = ext == null || !extWhiteList.contains(ext);
if (extInvalid) {
System.out.println("非法文件,禁止上传");
return null;
}
}

// 2. 限制上传文件 content-type
if (limitContentType) {
condition.put("string_like_if_exist", new HashMap<String, String>() {{
put("cos:content-type", "image/*");
}});
}

// 3. 限制上传文件大小(只对简单上传生效)
if (limitContentLength) {
condition.put("numeric_less_than_equal", new HashMap<String, Long>() {{
put("cos:content-length", 5L * 1024 * 1024);
}});
}
String key = generateCosKey(ext);
String resource = "qcs::cos:" + region + ":uid/" + appId + ':' + bucket + '/' + key;
List allowActions = Arrays.asList(
// 简单上传
"name/cos:PutObject"
);

// 构建policy
Map<String, Object> policy = new HashMap();
policy.put("version", "2.0");
Map<String, Object> statement = new HashMap();
statement.put("action", allowActions);
statement.put("effect", "allow");
List<String> resources = Arrays.asList(
resource
);
statement.put("resource", resources);
statement.put("condition", condition);
policy.put("statement", Arrays.asList(statement));


// 构建config
TreeMap <String,Object> config = new TreeMap<String, Object>();
config.put("secretId",secretId);
config.put("secretKey",secretKey);
config.put("proxy",proxy);
config.put("duration",durationSeconds);
config.put("bucket",bucket);
config.put("region",region);
config.put("key",key);
config.put("policy",Jackson.toJsonPrettyString(policy));
return config;
}

public TreeMap <String,Object> getKeyAndCredentials() {
TreeMap config = this.getConfig();
try {
Response response = CosStsClient.getCredential(config);
TreeMap <String,Object> credential = new TreeMap<String, Object>();
TreeMap <String,Object> credentials = new TreeMap<String, Object>();
credentials.put("tmpSecretId",response.credentials.tmpSecretId);
credentials.put("tmpSecretKey",response.credentials.tmpSecretKey);
credentials.put("sessionToken",response.credentials.sessionToken);
credential.put("startTime",response.startTime);
credential.put("expiredTime",response.expiredTime);
credential.put("requestId",response.requestId);
credential.put("expiration",response.expiration);
credential.put("credentials",credentials);
credential.put("bucket",config.get("bucket"));
credential.put("region",config.get("region"));
credential.put("key",config.get("key"));
return credential;
} catch (Exception e) {
e.printStackTrace();
throw new IllegalArgumentException("no valid secret !");
}
}
/**
* 基本的临时密钥申请示例,适合对一个桶内的一批对象路径,统一授予一批操作权限
*/
@Test
public void testGetKeyAndCredentials() {
TreeMap <String,Object> credential = this.getKeyAndCredentials();
TreeMap <String,Object> credentials = (TreeMap<String, Object>) credential.get("credentials");
try {


String tmpSecretId = (String) credentials.get("tmpSecretId");
String tmpSecretKey = (String) credentials.get("tmpSecretKey");
String sessionToken = (String) credentials.get("sessionToken");
Date expiredTime = new Date((Long) credential.get("expiredTime"));
String key = (String) credential.get("key");
String bucket = (String) credential.get("bucket");
String region = (String) credential.get("region");

COSCredentials cred = new BasicSessionCredentials(tmpSecretId, tmpSecretKey, sessionToken);
ClientConfig clientConfig = new ClientConfig();
clientConfig.setRegion(new Region(region));

COSClient cosClient = new COSClient(cred, clientConfig);


Map<String, String> headers = new HashMap<String,String>();
Map<String, String> params = new HashMap<String,String>();
params.put("x-cos-security-token",sessionToken);
URL url = cosClient.generatePresignedUrl(bucket, key, expiredTime, HttpMethodName.PUT, headers, params);
String host = "https://" + url.getHost();
String query = url.toString().split("\\\\?")[1];
String sign = query.split("&x-cos-security-token")[0];
TreeMap <String,Object> result = new TreeMap<String, Object>();
result.put("cosHost",host);
result.put("cosKey",key);
result.put("authorization",sign);
result.put("securityToken",sessionToken);
System.out.println(Jackson.toJsonPrettyString(result));
} catch (Exception e) {
e.printStackTrace();
throw new IllegalArgumentException("no valid sign !");
}
}
}
完整代码可参考 示例代码

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Mail;
using COSSTS;
using COSXML;
using COSXML.Auth;
using COSXML.Model.Tag;
using Newtonsoft.Json;
using Formatting = System.Xml.Formatting;


namespace COSSnippet
{
public class ServerSign
{
//永久密钥
string secretId = "";
string secretKey = "";

string bucket = "bucket-1250000000";
string appId = "1250000000";
string region = "ap-guangzhou";
string filename = "test.jpg";
string method = "put";
int time = 1800;
// 限制
Boolean limitExt = false; // 限制上传文件后缀
List<string> extWhiteList = new List<String> { "jpg", "jpeg", "png", "gif", "bmp" }; // 限制的上传后缀
Boolean limitContentType = false; // 限制上传 contentType
Boolean limitContentLength = false; // 限制上传文件大小

public string generateCosKey(string ext)
{
DateTime date = DateTime.Now;
int m = date.Month;
string ymd = $"{date.Year}{(m < 10 ? $"0{m}" : m.ToString())}{date.Day}";
Random random = new Random();
string r = random.Next(0, 1000000).ToString("D6"); // 生成6位随机数,前面补零
string cosKey = $"file/{ymd}/{ymd}_{r}.{(string.IsNullOrEmpty(ext) ? "" : ext)}";
return cosKey;
}
public Dictionary<string, object> getConfig()
{
Dictionary<string, object> config = new Dictionary<string, object>();
string[] allowActions = new string[] { // 允许的操作范围,这里以上传操作为例
"name/cos:PutObject"
};
string[] segments = filename.Split(".");
string ext = segments.Length > 0 ? segments[segments.Length - 1] : string.Empty;
string key = generateCosKey(ext);
string resource = $"qcs::cos:{region}:uid/{appId}:{bucket}/{key}";

var condition = new Dictionary<string, object>();
// 1. 限制上传文件后缀
if (limitExt)
{
var extInvalid = string.IsNullOrEmpty(ext) || !extWhiteList.Contains(ext);
if (extInvalid)
{
Console.WriteLine("非法文件,禁止上传");
return null;
}
}

// 2. 限制上传文件 content-type
if (limitContentType)
{
condition["string_like_if_exist"] = new Dictionary<string, string>
{
{ "cos:content-type", "image/*" } // 只允许上传 content-type 为图片类型
};
}

// 3. 限制上传文件大小(只对简单上传生效)
if (limitContentLength)
{
condition["numeric_less_than_equal"] = new Dictionary<string, long>
{
{ "cos:content-length", 5 * 1024 * 1024 } // 上传大小限制不能超过 5MB
};
}

var policy = new Dictionary<string, object>
{
{ "version", "2.0" },
{ "statement", new List<Dictionary<string, object>>
{
new Dictionary<string, object>
{
{ "action", allowActions },
{ "effect", "allow" },
{ "resource", new List<string>
{
resource,
}
},
{ "condition", condition }
}
}
}
};

// 序列化为 JSON 并输出
string jsonPolicy = JsonConvert.SerializeObject(policy);
config.Add("bucket", bucket);
config.Add("region", region);
config.Add("durationSeconds", time);

config.Add("secretId", secretId);
config.Add("secretKey", secretKey);
config.Add("key", key);
config.Add("policy", jsonPolicy);
return config;
}
// 获取联合身份临时访问凭证 https://cloud.tencent.com/document/product/1312/48195
public Dictionary<string, object> GetCredential()
{

var config = getConfig();
//获取临时密钥
Dictionary<string, object> credential = STSClient.genCredential(config);
Dictionary<string, object> credentials = JsonConvert.DeserializeObject<Dictionary<string, object>>(JsonConvert.SerializeObject((object) credential["Credentials"]));
Dictionary<string, object> credentials1 = new Dictionary<string, object>();
credentials1.Add("tmpSecretId",credentials["TmpSecretId"]);
credentials1.Add("tmpSecretKey",credentials["TmpSecretKey"]);
credentials1.Add("sessionToken",credentials["Token"]);
Dictionary<string, object> dictionary1 = new Dictionary<string, object>();
dictionary1.Add("credentials",credentials1);
dictionary1.Add("startTime",credential["StartTime"]);
dictionary1.Add("requestId",credential["RequestId"]);
dictionary1.Add("expiration",credential["Expiration"]);
dictionary1.Add("expiredTime",credential["ExpiredTime"]);
dictionary1.Add("bucket",config["bucket"]);
dictionary1.Add("region",config["region"]);
dictionary1.Add("key",config["key"]);
return dictionary1;
}
static void Main(string[] args)
{
ServerSign m = new ServerSign();
Dictionary<string, object> result = m.GetCredential();
Dictionary<string, object> credentials = (Dictionary<string, object>)result["credentials"];
string tmpSecretId = (string)credentials["tmpSecretId"];
string tmpSecretKey = (string)credentials["tmpSecretKey"];
string sessionToken = (string)credentials["sessionToken"];
string bucket = (string)result["bucket"];
string region = (string)result["region"];
string key = (string)result["key"];
long expiredTime = (long)result["expiredTime"];
int startTime = (int)result["startTime"];
QCloudCredentialProvider cosCredentialProvider = new DefaultSessionQCloudCredentialProvider(
tmpSecretId, tmpSecretKey, expiredTime, sessionToken);
CosXmlConfig config = new CosXmlConfig.Builder()
.IsHttps(true) //设置默认 HTTPS 请求
.SetRegion(region) //设置一个默认的存储桶地域
.SetDebugLog(true) //显示日志
.Build(); //创建 CosXmlConfig 对象
CosXml cosXml = new CosXmlServer(config, cosCredentialProvider);
PreSignatureStruct preSignatureStruct = new PreSignatureStruct();
preSignatureStruct.appid = m.appId;//"1250000000";
preSignatureStruct.region = region;//"COS_REGION";
preSignatureStruct.bucket = bucket;//"examplebucket-1250000000";
preSignatureStruct.key = "exampleObject"; //对象键
preSignatureStruct.httpMethod = "PUT"; //HTTP 请求方法
preSignatureStruct.isHttps = true; //生成 HTTPS 请求 URL
preSignatureStruct.signDurationSecond = 600; //请求签名时间为 600s
preSignatureStruct.headers = null; //签名中需要校验的 header
preSignatureStruct.queryParameters = null; //签名中需要校验的 URL 中请求参数
//上传预签名 URL (使用永久密钥方式计算的签名 URL)
Dictionary<string, string> queryParameters = new Dictionary<string, string>();
queryParameters.Add("x-cos-security-token",sessionToken);
Dictionary<string, string> headers = new Dictionary<string, string>();
string authorization = cosXml.GenerateSign(m.method,key,queryParameters,headers,expiredTime - startTime,expiredTime - startTime);

string host = "https://" + bucket + ".cos." + region + ".myqcloud.com";
Dictionary<string, object> response = new Dictionary<string, object>();
response.Add("cosHost",host);
response.Add("cosKey",key);
response.Add("authorization",authorization);
response.Add("securityToken",sessionToken);
Console.WriteLine($"{JsonConvert.SerializeObject(response)}");
}
}
}