这篇档介绍如何搭建安全的临时签名服务。
一、方案优势
·权限安全:可以有效限定安全的权限范围,只能用于上传指定的一个文件路径。
·路径安全:由服务端决定随机的 COS 文件路径,可以有效避免已有文件被覆盖的问题和安全风险。
·传输安全:在服务端生成签名,避免临时密钥传输过程中泄露的风险。
二、临时签名服务搭建
临时密钥(临时访问凭证)是通过 CAM 云 API 提供的接口,获取到权限受限的密钥。当需要发起 COS API 请求时,需要用到获取临时密钥接口返回信息中 的 TmpSecretId、TmpSecretKey 和 Token 三个字段,用于计算签名。
在请求服务端签名时,会先获取临时密钥,然后用临时密钥生成签名,返回客户端。
以下为各语言的示例代码:
Node.jsGoPHPPythonJava.NET(C#)完整代码可参考 示例代码。导入 STS 包请参见 Node.js STS 包导入。// 临时密钥服务例子var express = require('express');var crypto = require('crypto');var STS = require('qcloud-cos-sts');// 配置参数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", "name/cos:InitiateMultipartUpload", "name/cos:ListMultipartUploads", "name/cos:ListParts", "name/cos:UploadPart", "name/cos:CompleteMultipartUpload" ], "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); 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');完整代码可参考 示例代码。导入 STS 包请参见 Go STS 包导入。package mainimport ( "context" "encoding/json" "errors" "fmt" "github.com/tencentyun/cos-go-sdk-v5" sts "github.com/tencentyun/qcloud-cos-sts-sdk/go" "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() ymd := fmt.Sprintf("%d%02d%d", date.Year(), int(date.Month()), 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: "1250000000", 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: "test-1250000000", 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{}, } // token 请求时需要放到 header 中 opt.Header.Add("x-cos-security-token", token) signature := c.Object.GetSignature(ctx, http.MethodPut, name, tak, tsk, time.Hour, opt, true) 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))}完整代码可参考 示例代码。导入 STS 包请参见 PHP STS 包导入。<?phprequire_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();完整代码可参考 示例代码。导入 STS 包请参见 Python STS 包导入。#!/usr/bin/env python# coding=utf-8import jsonimport osimport datetimeimport randomfrom urllib.parse import urlencodefrom qcloud_cos import CosConfig, CosS3Clientfrom sts.sts import Stsif __name__ == '__main__': # 配置参数 config = { "filename":"test.jpg", "appId": "1250000000", "secretId": os.getenv("SecretId"), "secretKey": os.getenv("SecretKey"), "proxy": os.getenv("Proxy"), "durationSeconds": 1800, "bucket": "examplebucket-1250000000", "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") – credential_dic.get("startTime"), "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))导入 Java STS 包请参见 Java STS 包导入,导入 Java COS 包请参见 Java COS 包导入。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 !"); } }}完整代码可参考 示例代码。导入 STS 包请参见 .NET STS 包导入。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)}"); } }}