JavaScript 结合 Go 实现 临时密钥(STS)

背景介绍

最近有个项目,需要通过网页上传文件到对象存储中,在查看COS快速入门时,文档建议使用获取临时密钥:

由于固定密钥放在前端会有安全风险,正式部署时我们推荐使用临时密钥的方式,实现过程为:前端首先请求服务端,服务端使用固定密钥调用 STS 服务申请临时密钥(具体内容请参见 临时密钥生成和使用指引 文档),然后返回临时密钥到前端使用。

没想到这个过程一言难尽啊。

开箱即用

先贴代码,以备后用,注意:这里的代码仅适合JavaScript和Go配合,特别是前端代码,和官网例子也是有区别的

后端采用gin框架,这里假设绑定到URL地址为/stsr.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))
  }
})

开始吐槽

接下来是吐槽时间:

Go SDK中对CredentialResultCredentials的定义如下

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
});

这里面tmpSecretIdtmpSecretKey等等所有的字段都是小写开头的,但是Go SDK中定义却是大写开头的,更坑的是,sessionToken这个字段在Go里面直接变成了Token。所以前文提供的javascript代码都修复了这些问题。

另外,文档中建议按照最小权限原则进行授权,但是COS API 授权策略使用指引居然没有列出所有的权限,搞得我干脆给了所有权限。

Leave a Comment

Back to Top