1、背景介绍
最近有个项目,需要通过网页上传文件到对象存储中,在查看COS快速入门时,文档建议使用获取临时密钥:
由于固定密钥放在前端会有安全风险,正式部署时我们推荐使用临时密钥的方式,实现过程为:前端首先请求服务端,服务端使用固定密钥调用 STS 服务申请临时密钥(具体内容请参见 临时密钥生成和使用指引 文档),然后返回临时密钥到前端使用。
没想到这个过程一言难尽啊。
2、开箱即用
先贴代码,以备后用,注意:这里的代码仅适合JavaScript和Go配合,特别是前端代码,和官网例子也是有区别的。
后端采用gin框架,这里假设绑定到URL地址为/api/sts
,r.POST("/sts", tencentSTS)
这段代码授予了临时密钥所有的权限,实际使用时,建议按照最小权限原则进行授权,详细权限可以参考COS API 授权策略使用指引。
package api
import (
"github.com/gin-gonic/gin"
"github.com/tencentyun/qcloud-cos-sts-sdk/go"
"strings"
"time"
)
type STSRequest struct {
Region string
Bucket string
}
func tencentSTS(c *gin.Context) {
var request STSRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(500, err)
return
}
// 云 API 密钥 SecretId 建议通过环境变量或者本地文件来读取
secretId := "<SecretId>"
// 云 API 密钥 SecretKey 建议通过环境变量或者本地文件来读取
secretKey := "<SecretKey>"
appid := request.Bucket[strings.LastIndex(request.Bucket, "-")+1:]
bucket := request.Bucket
region := request.Region
client := sts.NewClient(secretId, secretKey, nil)
// 策略概述 https://cloud.tencent.com/document/product/436/18023
opt := &sts.CredentialOptions{
DurationSeconds: int64(time.Hour.Seconds()),
Region: "ap-guangzhou",
Policy: &sts.CredentialPolicy{
Statement: []sts.CredentialPolicyStatement{
{
Action: []string{
// 所有权限
"*",
},
Effect: "allow",
Resource: []string{
//这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的具体路径,例子: a.jpg 或者 a/* 或者 * (使用通配符*存在重大安全风险, 请谨慎评估使用)
//存储桶的命名格式为 BucketName-APPID,此处填写的 bucket 必须为此格式
"qcs::cos:" + region + ":uid/" + appid + ":" + bucket + "/*",
},
},
},
},
}
if res, err := client.GetCredential(opt); err != nil {
c.JSON(500, err)
} else {
c.JSON(200, res)
}
}
const cos = new COS({
getAuthorization: function (options, callback) {
// 异步获取临时密钥
// 服务端 JS 和 PHP 例子:https://github.com/tencentyun/cos-js-sdk-v5/blob/master/server/
// 服务端其他语言参考 COS STS SDK :https://github.com/tencentyun/qcloud-cos-sts-sdk
// STS 详细文档指引看:https://cloud.tencent.com/document/product/436/14048
const url = '/api/sts' // url 替换成您自己的后端服务
const xhr = new XMLHttpRequest()
xhr.open('POST', url, true)
xhr.onload = function () {
try {
const data = JSON.parse(this.responseText)
const credentials = data.Credentials
if (!data || !credentials) {
return console.error('credentials invalid:\n' + JSON.stringify(data, null, 2))
}
callback({
TmpSecretId: credentials.TmpSecretId,
TmpSecretKey: credentials.TmpSecretKey,
SecurityToken: credentials.Token,
// 建议返回服务器时间作为签名的开始时间,避免用户浏览器本地时间偏差过大导致签名错误
StartTime: data.StartTime, // 时间戳,单位秒,如:1580000000
ExpiredTime: data.ExpiredTime // 时间戳,单位秒,如:1580000000
})
} catch (e) {
console.error('credentials invalid:' + e)
}
}
xhr.send(JSON.stringify(options))
}
})
3、开始吐槽
接下来是吐槽时间:
Go SDK中对CredentialResult
和Credentials
的定义如下
type Credentials struct {
TmpSecretID string `json:"TmpSecretId,omitempty"`
TmpSecretKey string `json:"TmpSecretKey,omitempty"`
SessionToken string `json:"Token,omitempty"`
}
type CredentialResult struct {
Credentials *Credentials `json:"Credentials,omitempty"`
ExpiredTime int `json:"ExpiredTime,omitempty"`
Expiration string `json:"Expiration,omitempty"`
StartTime int `json:"StartTime,omitempty"`
RequestId string `json:"RequestId,omitempty"`
Error *CredentialError `json:"Error,omitempty"`
}
官网JavaScript代码如下:
callback({
TmpSecretId: credentials.tmpSecretId,
TmpSecretKey: credentials.tmpSecretKey,
SecurityToken: credentials.sessionToken,
// 建议返回服务器时间作为签名的开始时间,避免用户浏览器本地时间偏差过大导致签名错误
StartTime: data.startTime, // 时间戳,单位秒,如:1580000000
ExpiredTime: data.expiredTime, // 时间戳,单位秒,如:1580000000
});
这里面tmpSecretId
、tmpSecretKey
等等所有的字段都是小写开头的,但是Go SDK中定义却是大写开头的,更坑的是,sessionToken
这个字段在Go里面直接变成了Token
。所以前文提供的javascript代码都修复了这些问题。
另外,文档中建议按照最小权限原则进行授权,但是COS API 授权策略使用指引居然没有列出所有的权限,搞得我干脆给了所有权限。